Auth
Base URL: https://channel.ozzieapp.com/v1
The Auth endpoints authenticate end users via a 6-digit OTP code delivered by WhatsApp or email. This is distinct from API client authentication (which uses a static Bearer token) β OTP is for the human user inside your app.
For the conceptual overview of how OTP fits into the full onboarding flow, see Architecture.
OTP detailsβ
| Property | Value |
|---|---|
| Code length | 6 digits |
| TTL | 10 minutes |
| Single-use | Yes β invalidated on first successful verify |
| Storage | bcrypt-hashed |
identifier | E.164 phone (e.g. +5511999990000) or email address |
channel auto-detection | E.164 phone β whatsapp; email address β email |
POST /v1/auth/otp/requestβ
Generates a 6-digit OTP code and delivers it to the user via WhatsApp or email.
Request bodyβ
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Phone in E.164 format or email address |
channel | string | No | "whatsapp" or "email". Auto-detected from identifier if omitted. |
language | string | No | "pt" | "en" | "es" β language of the OTP message. Defaults to "en". |
- curl
- Node.js
curl -X POST https://channel.ozzieapp.com/v1/auth/otp/request \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": "+5511999990000",
"language": "pt"
}'
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://channel.ozzieapp.com/v1/auth/otp/request', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: '+5511999990000',
language: 'pt',
}),
});
const { data } = await response.json();
console.log(data.masked); // "+55119****0000"
console.log(data.expires_at);
Response (200 OK):
{
"object": "otp_request",
"data": {
"channel": "whatsapp",
"masked": "+55119****0000",
"expires_at": "2026-05-16T14:40:00Z"
}
}
| Field | Description |
|---|---|
channel | Delivery channel used: "whatsapp" or "email" |
masked | Masked version of the identifier β safe to display to the user for confirmation |
expires_at | ISO 8601 timestamp when the code expires (10 minutes from issuance) |
POST /v1/auth/otp/verifyβ
Verifies the OTP code the user entered. On success, returns the Ozzie user_id and the user's current onboarding_stage.
Request bodyβ
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Same phone or email used in the request step |
code | string | Yes | 6-digit code the user received |
- curl
- Node.js
curl -X POST https://channel.ozzieapp.com/v1/auth/otp/verify \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": "+5511999990000",
"code": "482910"
}'
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://channel.ozzieapp.com/v1/auth/otp/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: '+5511999990000',
code: '482910',
}),
});
if (!response.ok) {
const { error } = await response.json();
// error.code === 'INVALID_CODE' or 'CODE_EXPIRED'
throw new Error(error.code);
}
const { data } = await response.json();
// data.user_id = "external:usr_8821"
// data.core_user_id = "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE"
// data.onboarding_stage = "financial_intake"
Response (200 OK):
{
"object": "otp_verify",
"data": {
"user_id": "external:usr_8821",
"core_user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"onboarding_stage": "financial_intake"
}
}
| Field | Description |
|---|---|
user_id | The Ozzie user identifier in external:{id} format β use this in subsequent path params |
core_user_id | The internal Ozzie UUID |
onboarding_stage | Current stage: financial_intake | personality | plan | active |
Errorsβ
| Code | HTTP | When |
|---|---|---|
INVALID_CODE | 400 | The code does not match or has already been used |
CODE_EXPIRED | 400 | The code was issued more than 10 minutes ago |
NOT_FOUND | 404 | No pending OTP found for this identifier |
VALIDATION_ERROR | 400 | identifier or code field is missing or malformed |
Full onboarding flowβ
After a successful verify, use onboarding_stage to route the user to the correct screen:
onboarding_stage | Next step |
|---|---|
financial_intake | Show the financial intake form β POST /v1/users/:id/financial-intake |
personality | Show the personality quiz β GET /v1/personality/questions + POST /v1/personality/score |
plan | Show the plan generation screen β POST /v1/users/:id/plan/generate, then PATCH /v1/users/:id/onboarding/complete |
active | User is fully onboarded β go directly to the main chat screen |
SESSION_SECRET and session cookie management are your app's responsibility. Ozzie does not issue session tokens β it only validates the OTP code and returns the data your app needs to bootstrap its own session.