Auth
These routes require a dashboard-issued API key in x-api-key. No route in this section requires an API-key permission flag.
Endpoints#
| Method | Path | Additional context | Success |
|---|---|---|---|
POST | /v1/auth/onboard | None | 201 |
GET | /v1/auth/referral/validate | None | 200 |
GET | /v1/auth/refresh-session | Bearer session | 200 |
POST | /v1/auth/send-otp | None | 201 |
POST | /v1/auth/verify-otp | None | 200 |
API-key validation failures return 403; rate-limit failures return 429. Session validation failures return 401. Error bodies use the standard success: false envelope and may originate in a called service; the route-owned errors below are the stable errors defined directly by each route.
POST /v1/auth/onboard#
Creates a user, initial profile, and either a new team or a profile in a pending invitation’s team.
Headers: x-api-key (required). The API key may be app-, team-, or profile-scoped.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
email | string | Yes | Email; lowercased and trimmed. + sub-addressing is rejected for typical addresses (certain allowlisted domains may be accepted). |
firstName | string | Yes | Minimum 2 characters; trimmed. |
lastName | string | Yes | Minimum 2 characters; trimmed. |
username | string | Yes | Minimum 4 characters; letters, digits, _, and -; cannot start with api_; lowercased and trimmed. |
referralCode | string | No | Case-insensitive ^[A-Z0-9_-]{4}[0-9]{4}$; normalized to uppercase. |
country | string | Yes | Supported two-letter ISO country code; normalized to uppercase. |
language | enum | No | en, es; defaults to en when omitted. |
isBusiness | boolean | Yes | |
businessName | string | Conditional | Minimum 2 characters; required when isBusiness is true. |
teamMetadata | object | No | Defaults to {}. |
teamDescription | string | No | |
termsOfService | boolean | Yes | Must be true. |
Response — 201
{
"success": true,
"data": {
"team": { "id": "team-uuid" },
"profile": {
"id": "profile-uuid",
"nickname": "johndoe",
"suborgId": "opaque-organization-id"
},
"teamMember": {
"profileId": "profile-uuid",
"teamId": "team-uuid",
"role": "admin"
},
"invitationAccepted": false,
"walletCreated": true
}
}profile and teamMember are the objects returned by profile creation. Treat suborgId as opaque. walletCreated is false for country SV.
Route-owned errors
| Status | Code | Message |
|---|---|---|
400 | APP_V1_AUTH_ONBOARD_ROUTE_04 | Invalid referral code |
409 | APP_V1_AUTH_ONBOARD_ROUTE_01 | User already exists |
409 | APP_V1_AUTH_ONBOARD_ROUTE_05 | Username already taken |
500 | APP_V1_AUTH_ONBOARD_ROUTE_03 | Failed to get suborg ID |
Malformed bodies return 400. Failures propagated from user, team, profile, wallet, or organization creation return 500 with that operation’s error code.
GET /v1/auth/referral/validate#
Checks whether a normalized referral code exists.
Headers: x-api-key (required).
Query
| Field | Type | Required | Constraints |
|---|---|---|---|
referralCode | string | Yes | Trimmed, uppercased, and matched against ^[A-Z0-9_-]{4}[0-9]{4}$. |
Response — 200
{
"success": true,
"data": { "exists": true }
}Route-owned errors
| Status | Code | Message |
|---|---|---|
400 | APP_V1_AUTH_REFERRAL_VALIDATE_ROUTE_01 | Missing referralCode |
400 | APP_V1_AUTH_REFERRAL_VALIDATE_ROUTE_02 | Invalid referral code format |
Lookup failures return 500 with the propagated error.
GET /v1/auth/refresh-session#
Refreshes the current login session. Call it while the bearer session is valid.
Headers: x-api-key and Authorization: Bearer <session> (both required).
There are no path parameters, query parameters, or request body.
Response — 200
When a new session is minted:
{
"success": true,
"data": {
"session": "new-session-token",
"expirationTimestamp": 1760000000,
"userType": "individual"
}
}When the presented token and stored session differ, the route synchronizes the stored session instead:
{
"success": true,
"data": {
"message": "Session refreshed",
"expirationTimestamp": 1760000000,
"userType": "individual"
}
}expirationTimestamp can be omitted if the token cannot be decoded. userType is individual, business, or admin.
Route-owned errors
| Status | Code | Message |
|---|---|---|
404 | APP_V1_AUTH_REFRESH_SESSION_01 | Keypair not found |
500 | APP_V1_AUTH_REFRESH_SESSION_02 | Failed to create read write session |
POST /v1/auth/send-otp#
Sends a login OTP and returns the identifier and matching profiles needed by verification.
Headers: x-api-key (required).
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
email | string | Yes | Email; normalized to lowercase. |
identifier | string or null | No | Minimum 10 characters. Generated when omitted or null. |
language | enum or null | No | en, es; falls back to the user’s language, then en. |
userType | enum or null | No | individual, business, admin; filters returned profiles. |
Response — 201
{
"success": true,
"data": {
"otpId": "otp-id",
"identifier": "public-key-identifier",
"profiles": [
{
"id": "profile-uuid",
"nickname": "john_doe",
"team": "John's Team",
"userType": "individual"
}
]
}
}Route-owned errors
| Status | Code | Message |
|---|---|---|
404 | APP_V1_AUTH_SEND_OTP_ROUTE_01 | User not found |
404 | APP_V1_AUTH_SEND_OTP_ROUTE_02 | User has no profile for this app |
401 | APP_V1_AUTH_SEND_OTP_ROUTE_03 | No teams with OTC approval found for this user |
404 | APP_V1_AUTH_SEND_OTP_ROUTE_04 | User not found for requested userType |
Malformed bodies return 400. Access-gate and OTP-provider failures use their propagated status/error.
POST /v1/auth/verify-otp#
Verifies a nine-character-or-longer OTP and creates a session for the selected profile.
Headers: x-api-key (required).
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
otp | string | Yes | Minimum 9 characters. |
otpId | string | Yes | Non-empty. |
email | string | Yes | Valid email. |
identifier | string or null | No | Minimum 10 characters. |
profileId | string or null | Conditional | Required when the user has multiple profiles; non-empty. |
userType | enum or null | No | individual, business, admin; must match the selected profile. |
Response — 200
{
"success": true,
"data": {
"session": "session-token",
"orgId": "opaque-organization-id",
"country": "US",
"userType": "individual",
"teams": [
{
"teamId": "team-uuid",
"teamName": "John's Team",
"profileId": "profile-uuid",
"profileNickname": "john_doe",
"profileSuborgId": "opaque-organization-id",
"profileRole": "admin",
"userType": "individual",
"teamDefaultReceiverWallet": "0x...",
"teamProfileSuborgId": "opaque-organization-id"
}
]
}
}teamDefaultReceiverWallet may be null. Organization identifiers are opaque.
Route-owned errors
| Status | Code | Message |
|---|---|---|
404 | APP_V1_AUTH_VERIFY_OTP_ROUTE_01 | User not found |
404 | APP_V1_AUTH_VERIFY_OTP_ROUTE_02 | Multiple profiles found |
404 | APP_V1_AUTH_VERIFY_OTP_ROUTE_03 | Profile not found |
404 | APP_V1_AUTH_VERIFY_OTP_ROUTE_04 | Profile does not match requested userType |
Malformed bodies return 400. User lookup and OTP/session-provider failures are propagated; the provider verification failure is currently returned as 500.