Onboarding with OTP

Use this guide to onboard a new user and obtain a session for profile, team, KYC, and wallet calls.

Prerequisites#

  • Staging or production API key from the dashboard
  • User email that has not already been onboarded (for create)
  • Optional referral code in the form XXXX#### (4 characters + 4 digits)

Flow overview#

  1. Optional: validate referral code
  2. POST /v1/auth/onboard for new users
  3. POST /v1/auth/send-otp
  4. POST /v1/auth/verify-otp → session + selected profile/team context
  5. GET /v1/auth/refresh-session when the session nears expiry

1. Validate a referral code (optional)#

curl -sS "https://api-stg.vudy.app/v1/auth/referral/validate?referralCode=ABCD1234" \
  -H "x-api-key: vudy_sandbox_YOUR_KEY"
const res = await fetch(
	"https://api-stg.vudy.app/v1/auth/referral/validate?referralCode=ABCD1234",
	{ headers: { "x-api-key": process.env.VUDY_API_KEY } },
);
const { data } = await res.json();
// { exists: true | false }

2. Onboard a new user#

Auth: API key only. Returns 201.

curl -sS https://api-stg.vudy.app/v1/auth/onboard \
  -H "x-api-key: vudy_sandbox_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alex@example.com",
    "firstName": "Alex",
    "lastName": "Rivera",
    "username": "alex-rivera",
    "country": "US",
    "language": "en",
    "isBusiness": false,
    "termsOfService": true
  }'
await fetch("https://api-stg.vudy.app/v1/auth/onboard", {
	method: "POST",
	headers: {
		"x-api-key": process.env.VUDY_API_KEY,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({
		email: "alex@example.com",
		firstName: "Alex",
		lastName: "Rivera",
		username: "alex-rivera", // min 4; a-z, 0-9, -, _; not api_*
		country: "US",
		language: "en",
		isBusiness: false,
		// isBusiness: true requires businessName
		termsOfService: true,
	}),
});

Emails with + sub-addressing are rejected for typical integrator accounts.

Success data (201) matches the auth reference shape:

{
  "team": { "id": "team-uuid" },
  "profile": {
    "id": "profile-uuid",
    "nickname": "alex-rivera",
    "suborgId": "opaque-organization-id"
  },
  "teamMember": {
    "profileId": "profile-uuid",
    "teamId": "team-uuid",
    "role": "admin"
  },
  "invitationAccepted": false,
  "walletCreated": true
}

Treat suborgId as opaque. walletCreated is false when the onboard country does not create a platform wallet (for example SV). If a pending invitation exists for the email, onboarding joins that team (invitationAccepted: true) instead of creating another one.

3. Send OTP#

curl -sS https://api-stg.vudy.app/v1/auth/send-otp \
  -H "x-api-key: vudy_sandbox_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"alex@example.com","language":"en"}'
const send = await fetch("https://api-stg.vudy.app/v1/auth/send-otp", {
	method: "POST",
	headers: {
		"x-api-key": process.env.VUDY_API_KEY,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({
		email: "alex@example.com",
		language: "en",
		// userType: "individual" | "business" | "admin" // optional filter
	}),
}).then((r) => r.json());

const { otpId, identifier, profiles } = send.data;

If GET /v1/maintenance returns maintenance, expect 503.

4. Verify OTP and create a session#

Pass profileId when the user has more than one profile.

curl -sS https://api-stg.vudy.app/v1/auth/verify-otp \
  -H "x-api-key: vudy_sandbox_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alex@example.com",
    "otpId": "OTP_ID",
    "otp": "123456789",
    "profileId": "PROFILE_UUID"
  }'
const verify = await fetch("https://api-stg.vudy.app/v1/auth/verify-otp", {
	method: "POST",
	headers: {
		"x-api-key": process.env.VUDY_API_KEY,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({
		email: "alex@example.com",
		otpId,
		otp: "123456789", // min 9 characters
		identifier,
		profileId: profiles[0].id, // required when multiple profiles
	}),
}).then((r) => r.json());

let session = verify.data.session;
const { teams, userType } = verify.data;

The session is bound to the selected profile and team. Calls using that session do not need x-profile-id or x-team-id, and those headers do not switch the session actor. Re-run OTP verification with another profileId to change context. See Session and context.

5. Refresh the session#

Auth: API key + session.

curl -sS https://api-stg.vudy.app/v1/auth/refresh-session \
  -H "x-api-key: vudy_sandbox_YOUR_KEY" \
  -H "Authorization: Bearer SESSION_JWT"

The response can contain a replacement data.session. Store and use it for subsequent calls:

const refreshed = await fetch(
	"https://api-stg.vudy.app/v1/auth/refresh-session",
	{
		headers: {
			"x-api-key": process.env.VUDY_API_KEY,
			Authorization: `Bearer ${session}`,
		},
	},
).then((r) => r.json());

session = refreshed.data.session ?? session;

Status / next steps#

AfterNext
Onboard + sessionProfiles and teams
Before paymentsKYC and KYB, Wallets
Endpoint fieldsAuth reference