Teams
All routes require x-api-key; none requires an API-key permission flag. Most team-management routes accept a valid session, x-profile-id plus x-team-id, or a team/profile-scoped API key (allowHeaderValidation). Session-only routes require Authorization: Bearer <session> and do not accept header-only context.
API-key failures return 403, rate limits return 429, and required-session failures return 401. Error bodies use the standard error envelope.
Roles used by invitation and role-update schemas: admin, member, viewer, developer, analyst, blocked, api. Admin is required for mutations unless noted otherwise.
Endpoints#
| Method | Path | Context | Admin | Success |
|---|---|---|---|---|
GET | /v1/teams/banks | Session, headers, or scoped key | No | 200 |
POST | /v1/teams/countries | Session, headers, or scoped key | Yes | 200 |
DELETE | /v1/teams/countries | Session, headers, or scoped key | Yes | 200 |
PATCH | /v1/teams/default-receiver-wallet | Session | Yes | 200 |
GET | /v1/teams/invitations | Session, headers, or scoped key | Yes | 200 |
POST | /v1/teams/invitations | Session, headers, or scoped key | Yes | 201 |
DELETE | /v1/teams/invitations/{id} | Session | Yes | 200 |
GET | /v1/teams/members | Session, headers, or scoped key | No | 200 |
DELETE | /v1/teams/members/{profileId} | Session, headers, or scoped key | Yes | 200 |
PATCH | /v1/teams/members/{profileId}/role | Session, headers, or scoped key | Yes | 200 |
PATCH | /v1/teams/metadata | Session, headers, or scoped key | Yes | 200 |
PATCH | /v1/teams/notification-email | Session, headers, or scoped key | Yes | 200 |
POST | /v1/teams/tax-info | Session, headers, or scoped key | Yes | 200 |
PATCH | /v1/teams/tax-info/default | Session, headers, or scoped key | Yes | 200 |
GET | /v1/teams/{teamId}/activity | Session | Membership | 200 |
POST | /v1/teams/{teamId}/image/upload-url | Session | Yes | 200 |
POST | /v1/teams/{teamId}/image | Session | Yes | 200 |
GET /v1/teams/members#
Lists members for the active team. Any team member with valid context may call this route.
Headers: x-api-key plus session, both x-profile-id and x-team-id, or a team/profile-scoped key.
{
"success": true,
"data": [
{
"profileId": "profile-uuid",
"role": "admin",
"joinedAt": "2026-07-28T17:00:00.000Z",
"addedBy": "profile-uuid",
"profile": {
"id": "profile-uuid",
"nickname": "johndoe",
"metadata": {},
"createdAt": "2026-07-28T17:00:00.000Z",
"updatedAt": "2026-07-28T17:00:00.000Z"
},
"user": {
"id": "user-uuid",
"username": "johndoe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"country": "US",
"language": "en"
},
"imageUrl": "/profile/profile-uuid/image-hash"
}
]
}imageUrl is omitted when the member has no profile image. Missing context returns 400 with APP_V1_TEAMS_MEMBERS_ROUTE_01 (Missing profile or team). Lookup failures return 500.
PATCH /v1/teams/members/{profileId}/role#
Admin only. Path {profileId} is the member to update.
| Field | Type | Required | Constraints |
|---|---|---|---|
role | enum | Yes | admin, member, viewer, developer, analyst, blocked, api |
{
"success": true,
"data": { "profileId": "profile-uuid", "role": "developer" }
}| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROLE_ROUTE_01 | Missing profile or team |
400 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROLE_ROUTE_02 | Missing profile ID |
403 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROLE_ROUTE_01 | Insufficient permissions |
400 | body parser code | Invalid body |
403 | propagated | Role update rejected (for example last-admin protection) |
DELETE /v1/teams/members/{profileId}#
Admin only. Blocks the member by setting role to blocked. There is no request body.
{
"success": true,
"data": { "profileId": "profile-uuid", "blocked": true }
}| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROUTE_01 | Missing profile or team |
400 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROUTE_02 | Missing profile ID |
403 | APP_V1_TEAMS_MEMBERS_PROFILEID_ROUTE_01 | Insufficient permissions |
403 | propagated | Block rejected |
POST /v1/teams/invitations#
Admin only. Creates a pending invitation. Invitation email is sent only when the calling API key includes the EMAIL permission; invitation creation still succeeds if email sending fails.
| Field | Type | Required | Constraints |
|---|---|---|---|
email | string | Yes | Valid email. |
role | enum | No | Defaults to member; same role enum as role updates. |
{
"success": true,
"data": {
"id": "invitation-uuid",
"teamId": "team-uuid",
"email": "user@example.com",
"role": "member",
"invitedBy": "profile-uuid",
"status": "pending",
"expiresAt": "2026-08-04T17:00:00.000Z",
"createdAt": "2026-07-28T17:00:00.000Z",
"updatedAt": "2026-07-28T17:00:00.000Z"
}
}| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_INVITATIONS_ROUTE_01 | Missing profile or team |
403 | APP_V1_TEAMS_INVITATIONS_ROUTE_01 | Insufficient permissions |
409 | APP_V1_TEAMS_INVITATIONS_ROUTE_02 | User already in team |
400 | body parser code | Invalid body |
500 | propagated | Invitation creation failed |
The 409 path triggers when the email already has one or more profiles for the same app (not only membership in this team).
GET /v1/teams/invitations#
Admin only. Returns pending invitations for the active team.
{
"success": true,
"data": [
{
"id": "invitation-uuid",
"teamId": "team-uuid",
"email": "user@example.com",
"role": "member",
"invitedBy": "profile-uuid",
"status": "pending",
"expiresAt": "2026-08-04T17:00:00.000Z",
"createdAt": "2026-07-28T17:00:00.000Z",
"updatedAt": "2026-07-28T17:00:00.000Z"
}
]
}| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_INVITATIONS_ROUTE_01 | Missing profile or team |
403 | APP_V1_TEAMS_INVITATIONS_ROUTE_03 | Insufficient permissions |
500 | propagated | Invitation list failed |
DELETE /v1/teams/invitations/{id}#
Session required. Admin only. Cancels by setting invitation status to rejected.
{
"success": true,
"data": {
"id": "invitation-uuid",
"status": "rejected",
"updatedAt": "2026-07-28T17:00:00.000Z"
}
}The success payload is the full updated invitation row returned by the status update.
| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_INVITATIONS_ID_ROUTE_01 | Missing profile or team |
403 | APP_V1_TEAMS_INVITATIONS_ID_ROUTE_02 | Insufficient permissions |
400 | APP_V1_TEAMS_INVITATIONS_ID_ROUTE_03 | Missing invitation ID |
404 | APP_V1_TEAMS_INVITATIONS_ID_ROUTE_04 | Invitation not found |
403 | APP_V1_TEAMS_INVITATIONS_ID_ROUTE_05 | Invitation doesn’t belong to team |
500 | propagated | Lookup or update failed |
POST /v1/teams/countries#
Admin only. Adds a supported ISO alpha-2 country to the team.
| Field | Type | Required | Constraints |
|---|---|---|---|
country | string | Yes | Exactly two characters after trim; uppercased and validated against supported country codes. |
Returns 200 with the updated team row (including countries and timestamps). Duplicate countries are treated as success and return the existing team.
| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_COUNTRIES_01 | Missing profile or team |
403 | APP_V1_TEAMS_COUNTRIES_02 | Insufficient permissions |
400 | APP_V1_TEAMS_COUNTRIES_03 | Unsupported country code |
400 | body parser code | Invalid body |
500 | propagated | Update failed |
DELETE /v1/teams/countries#
Admin only. Removes a country from the team. Body shape matches POST.
Returns 200 with the updated team row. Route-owned errors reuse APP_V1_TEAMS_COUNTRIES_01 / _02; unsupported-country validation is not applied on remove.
GET /v1/teams/banks#
Lists bank accounts for all profiles on the active team. The route does not enforce an admin role check.
{
"success": true,
"data": [
{
"profile": {
"id": "profile-uuid",
"nickname": "johndoe"
},
"banks": [
{
"id": "bank-uuid",
"accountAlias": "Main Checking",
"accountType": "CHECKING"
}
]
}
]
}Each item includes the full profile and bank rows returned by the database query; the example shows commonly used fields. Missing context returns 400 with APP_V1_TEAMS_BANKS_ROUTE_01. Lookup failures return 500.
PATCH /v1/teams/default-receiver-wallet#
Session required. Admin only.
| Field | Type | Required | Constraints |
|---|---|---|---|
defaultReceiverWallet | string or null | No | Omitted or null clears the default. |
Returns 200 with the updated team row. Missing context or insufficient permissions share code APP_V1_TEAMS_DEFAULT_RECEIVER_WALLET_ROUTE_01 (Missing profile or team context / Insufficient permissions). Malformed bodies return 400; update failures return 500.
PATCH /v1/teams/metadata#
Admin only. Updates team name, description, and/or custom metadata fields.
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | No | Non-empty if provided. |
description | string | No | |
| any other key | any JSON value | No | Merged into metadata. Keys devAccess and devAccessRequest are rejected. |
Returns 200 with the updated team object plus optional imageUrl derived from team image metadata.
| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_METADATA_ROUTE_01 | Missing profile or team |
403 | APP_V1_TEAMS_METADATA_ROUTE_01 | Insufficient permissions |
404 | APP_V1_TEAMS_METADATA_ROUTE_02 | Team not found |
400 | body parser code | Invalid body / restricted field |
500 | propagated | Name, description, metadata, or reload failed |
PATCH /v1/teams/notification-email#
Admin only.
| Field | Type | Required | Constraints |
|---|---|---|---|
notificationEmail | string or null | No | Valid email when provided; omit or null to clear. |
Returns 200 with the updated team row. Missing context is APP_V1_TEAMS_NOTIFICATION_EMAIL_ROUTE_01; insufficient permissions is _03.
POST /v1/teams/tax-info#
Admin only. Upserts a tax-info entry in team metadata.
| Field | Type | Required | Constraints |
|---|---|---|---|
country | string | Yes | Supported two-letter ISO code; uppercased. |
taxId | string | Yes | Trimmed; 1–64 chars; alphanumeric and -. |
name | string or null | No | 1–120 characters when provided. |
address | string or null | No | 1–240 characters when provided. |
setAsDefaultForCountry | boolean | No | |
setAsGlobalDefault | boolean | No |
Returns 200 with the updated team object (including nested metadata.taxInfo) plus optional imageUrl.
| Status | Code | Message |
|---|---|---|
400 | APP_V1_TEAMS_TAX_INFO_01 | Missing profile or team |
403 | APP_V1_TEAMS_TAX_INFO_02 | Admin required |
400 | body parser code | Invalid body |
500 | propagated | Update failed |
PATCH /v1/teams/tax-info/default#
Admin only. Sets the global default tax-info entry.
| Field | Type | Required | Constraints |
|---|---|---|---|
country | string | Yes | Supported two-letter ISO code; uppercased. |
taxId | string | Yes | Same constraints as tax-info create. |
Returns 200 with the updated team object plus optional imageUrl. Errors use APP_V1_TEAMS_TAX_INFO_DEFAULT_01 / _02.
GET /v1/teams/{teamId}/activity#
Session required. The session profile must belong to {teamId}.
Query
| Field | Type | Required | Constraints |
|---|---|---|---|
profileId | string | No | Filters activity to one profile. |
limit | number | No | Integer 1–100; defaults to 50. |
exclusiveStartKey | string | No | Pagination cursor from a previous response. |
{
"success": true,
"data": {
"items": [
{
"PK": "TEAM#team-uuid",
"SK": "TIMESTAMP#2026-07-28T17:00:00.000Z",
"ActionType": "transaction",
"ActionName": "tx_completed",
"Status": "completed",
"TeamId": "team-uuid",
"ProfileId": "profile-uuid",
"CreatedAt": "2026-07-28T17:00:00.000Z"
}
],
"lastEvaluatedKey": "opaque-pagination-cursor"
}
}Item fields vary by action type. lastEvaluatedKey is omitted when there is no next page.
| Status | Code | Message |
|---|---|---|
404 | APP_V1_TEAMS_ACTIVITY_01 | Team not found / Team not found or no access |
400 | APP_V1_TEAMS_ACTIVITY_03 | Invalid limit parameter. Must be between 1 and 100 |
500 | APP_V1_TEAMS_ACTIVITY_02 | Failed to get activity |
Team image#
Both routes require x-api-key, Authorization: Bearer <session>, and an admin profile whose team matches {teamId}. Allowed filename extensions are jpg, jpeg, png, webp, and gif; allowed MIME types are image/jpeg, image/jpg, image/png, image/webp, and image/gif.
POST /v1/teams/{teamId}/image/upload-url#
| Field | Type | Required |
|---|---|---|
fileName | string | Yes |
contentType | allowed MIME enum | Yes |
{
"success": true,
"data": {
"uploadUrl": "https://storage.example/signed-upload",
"filePath": "team-images/team-uuid/1760000000000-hash.jpg",
"expiresAt": "2026-07-28T18:00:00.000Z"
}
}| Status | Code | Message |
|---|---|---|
400 | V1_TEAM_IMAGE_UPLOAD_URL_01 | Missing team ID / Team ID mismatch |
401 | V1_TEAM_IMAGE_UPLOAD_URL_01 | Invalid session or team context |
403 | V1_TEAM_IMAGE_UPLOAD_URL_01 | Insufficient permissions - admin required |
400 | body parser code | Invalid body |
500 | V1_TEAM_IMAGE_UPLOAD_URL_02 | Failed to generate upload URL |
POST /v1/teams/{teamId}/image#
Call after uploading the file to the signed URL.
| Field | Type | Required |
|---|---|---|
filePath | non-empty string | Yes |
{
"success": true,
"data": { "imageUrl": "/team/team-uuid/image-hash" }
}Route-owned errors mirror the upload-url codes under V1_TEAM_IMAGE_CONFIRM_01 / _02.