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#

MethodPathAdditional contextSuccess
POST/v1/auth/onboardNone201
GET/v1/auth/referral/validateNone200
GET/v1/auth/refresh-sessionBearer session200
POST/v1/auth/send-otpNone201
POST/v1/auth/verify-otpNone200

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

FieldTypeRequiredConstraints
emailstringYesEmail; lowercased and trimmed. + sub-addressing is rejected for typical addresses (certain allowlisted domains may be accepted).
firstNamestringYesMinimum 2 characters; trimmed.
lastNamestringYesMinimum 2 characters; trimmed.
usernamestringYesMinimum 4 characters; letters, digits, _, and -; cannot start with api_; lowercased and trimmed.
referralCodestringNoCase-insensitive ^[A-Z0-9_-]{4}[0-9]{4}$; normalized to uppercase.
countrystringYesSupported two-letter ISO country code; normalized to uppercase.
languageenumNoen, es; defaults to en when omitted.
isBusinessbooleanYes
businessNamestringConditionalMinimum 2 characters; required when isBusiness is true.
teamMetadataobjectNoDefaults to {}.
teamDescriptionstringNo
termsOfServicebooleanYesMust 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

StatusCodeMessage
400APP_V1_AUTH_ONBOARD_ROUTE_04Invalid referral code
409APP_V1_AUTH_ONBOARD_ROUTE_01User already exists
409APP_V1_AUTH_ONBOARD_ROUTE_05Username already taken
500APP_V1_AUTH_ONBOARD_ROUTE_03Failed 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

FieldTypeRequiredConstraints
referralCodestringYesTrimmed, uppercased, and matched against ^[A-Z0-9_-]{4}[0-9]{4}$.

Response — 200

{
	"success": true,
	"data": { "exists": true }
}

Route-owned errors

StatusCodeMessage
400APP_V1_AUTH_REFERRAL_VALIDATE_ROUTE_01Missing referralCode
400APP_V1_AUTH_REFERRAL_VALIDATE_ROUTE_02Invalid 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

StatusCodeMessage
404APP_V1_AUTH_REFRESH_SESSION_01Keypair not found
500APP_V1_AUTH_REFRESH_SESSION_02Failed 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

FieldTypeRequiredConstraints
emailstringYesEmail; normalized to lowercase.
identifierstring or nullNoMinimum 10 characters. Generated when omitted or null.
languageenum or nullNoen, es; falls back to the user’s language, then en.
userTypeenum or nullNoindividual, 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

StatusCodeMessage
404APP_V1_AUTH_SEND_OTP_ROUTE_01User not found
404APP_V1_AUTH_SEND_OTP_ROUTE_02User has no profile for this app
401APP_V1_AUTH_SEND_OTP_ROUTE_03No teams with OTC approval found for this user
404APP_V1_AUTH_SEND_OTP_ROUTE_04User 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

FieldTypeRequiredConstraints
otpstringYesMinimum 9 characters.
otpIdstringYesNon-empty.
emailstringYesValid email.
identifierstring or nullNoMinimum 10 characters.
profileIdstring or nullConditionalRequired when the user has multiple profiles; non-empty.
userTypeenum or nullNoindividual, 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

StatusCodeMessage
404APP_V1_AUTH_VERIFY_OTP_ROUTE_01User not found
404APP_V1_AUTH_VERIFY_OTP_ROUTE_02Multiple profiles found
404APP_V1_AUTH_VERIFY_OTP_ROUTE_03Profile not found
404APP_V1_AUTH_VERIFY_OTP_ROUTE_04Profile 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.