Authentication
The Ozzie API uses HTTP Bearer authentication. Every request must include an Authorization header with a base64-encoded token derived from your client_id and client_secret.
Getting your credentialsβ
To get API credentials, either:
- Contact the Ozzie team at commercial@ozzieapp.com to discuss your use case and get access
- Log in to the Ozzie dashboard and navigate to Settings β API Keys to generate a
client_idandclient_secretpair
You will receive two values:
| Value | Example | Description |
|---|---|---|
client_id | ozz_client_a1b2c3d4 | A stable identifier for your API client. Safe to log. |
client_secret | sk_live_xK9mP2qR7tL... | A secret key. Treat this like a password. Never expose it. |
Your client_secret is shown only once at generation time. Store it securely immediately β if you lose it, you will need to rotate your credentials.
How authentication worksβ
Ozzie uses HTTP Basic Auth encoded as a Bearer token. The token is the base64 encoding of the string client_id:client_secret (colon-separated, no spaces).
token = base64("ozz_client_a1b2c3d4:sk_live_xK9mP2qR7tL...")
Authorization: Bearer <token>
This is a single, static token you generate once and reuse across all requests. You do not need to exchange it for a session token or refresh it on a schedule β it is valid until you rotate your credentials.
Encoding your tokenβ
- Shell
- Node.js
- Python
# Encode your credentials
CLIENT_ID="ozz_client_a1b2c3d4"
CLIENT_SECRET="sk_live_xK9mP2qR7tL..."
TOKEN=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64)
echo $TOKEN
# ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu...
const clientId = process.env.OZZIE_CLIENT_ID;
const clientSecret = process.env.OZZIE_CLIENT_SECRET;
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
console.log(token);
// ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu...
import base64
import os
client_id = os.environ["OZZIE_CLIENT_ID"]
client_secret = os.environ["OZZIE_CLIENT_SECRET"]
token = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
print(token)
# ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu...
Making authenticated requestsβ
Once you have the token, attach it to every request in the Authorization header:
- curl
- Node.js
- Python
curl https://api.ozzieapp.com/v1/api/v1/users \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu..." \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "user_123",
"name": "Alex Johnson",
"language": "en"
}'
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://api.ozzieapp.com/v1/api/v1/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
external_user_id: 'user_123',
name: 'Alex Johnson',
language: 'en',
}),
});
const data = await response.json();
console.log(data);
import base64
import os
import httpx
token = base64.b64encode(
f"{os.environ['OZZIE_CLIENT_ID']}:{os.environ['OZZIE_CLIENT_SECRET']}".encode()
).decode()
response = httpx.post(
"https://api.ozzieapp.com/v1/api/v1/users",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"external_user_id": "user_123",
"name": "Alex Johnson",
"language": "en",
},
)
print(response.json())
Build the token once at application startup and reuse it across all requests. There is no expiry β you don't need to regenerate it per-request.
Rate limitingβ
Every API response includes rate limit headers so you can track your usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | The maximum number of requests allowed in the current window |
X-RateLimit-Remaining | How many requests you have left in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets and your limit replenishes |
Example response headers:
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1746489600
When you hit the rate limit, the API returns a 429 status. Back off and retry after the timestamp in X-RateLimit-Reset.
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit for this window. Please retry after the reset time.",
"details": [
{
"reset_at": 1746489600,
"limit": 1000,
"window": "1 minute"
}
]
}
}
Rate limits vary by tier. Free tier clients have lower limits than Pro or Enterprise clients. Contact commercial@ozzieapp.com if you need higher throughput.
Authentication error codesβ
| HTTP Status | Error Code | When it happens |
|---|---|---|
401 Unauthorized | UNAUTHORIZED | The Authorization header is missing, malformed, or contains invalid credentials |
429 Too Many Requests | RATE_LIMIT_EXCEEDED | Your client has sent more requests than allowed in the current window |
Example 401 response:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing authorization credentials.",
"details": []
}
}
Common causes of a 401:
- Forgot to include the
Authorizationheader - Encoded
client_id:client_secretwith spaces or extra characters - Using a
client_secretthat was rotated or revoked - Sending the raw
client_id:client_secretstring instead of the base64-encoded version
End-user authentication (OTP)β
Consumer apps authenticate their end users via a 6-digit OTP code delivered by WhatsApp or email. This flow is handled entirely by the Channel API β your server calls channel.ozzieapp.com on behalf of the user.
The OTP flow is separate from API client auth. Your client_id / client_secret are server-to-server credentials. OTP is for authenticating the human end user inside your app.
The full OTP flowβ
1. Register the user (once)
β
βΌ
POST /v1/users β receives Ozzie user_id
β
βΌ
2. Request an OTP code
β
βΌ
POST /v1/auth/otp/request β code sent to user via WhatsApp or email
β
βΌ
3. User enters the code in your UI
β
βΌ
POST /v1/auth/otp/verify β returns user_id + onboarding_stage
β
βΌ
4. Your app creates a session for the user
(SESSION_SECRET and session management are your app's concern)
OTP detailsβ
| Property | Value |
|---|---|
| Code length | 6 digits |
| TTL | 10 minutes |
| Single-use | Yes β code is invalidated on first successful verify |
| Storage | bcrypt-hashed in DB |
identifier | Phone in E.164 format (e.g. +5511999990000) or email address |
channel | "whatsapp" or "email" β auto-detected from identifier if omitted |
TypeScript exampleβ
const BASE = 'https://channel.ozzieapp.com/v1';
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
// Step 1 β Register the user (skip if already registered; handle 409 gracefully)
const registerRes = await fetch(`${BASE}/users`, {
method: 'POST',
headers,
body: JSON.stringify({
external_user_id: 'usr_8821', // your own user ID β do NOT include "external:" prefix
name: 'Maria Santos',
phone: '+5511999990000', // E.164 format
language: 'pt',
}),
});
if (!registerRes.ok && registerRes.status !== 409) {
throw new Error('User registration failed');
}
// Step 2 β Request OTP
const otpRes = await fetch(`${BASE}/auth/otp/request`, {
method: 'POST',
headers,
body: JSON.stringify({
identifier: '+5511999990000',
language: 'pt',
// channel: 'whatsapp' // optional β auto-detected from E.164 phone
}),
});
const { data: otpData } = await otpRes.json();
// otpData = { channel: "whatsapp", masked: "+55119****0000", expires_at: "2026-05-16T..." }
// Step 3 β Verify OTP (called after the user submits the code in your UI)
async function verifyOtp(code: string) {
const verifyRes = await fetch(`${BASE}/auth/otp/verify`, {
method: 'POST',
headers,
body: JSON.stringify({
identifier: '+5511999990000',
code,
}),
});
if (!verifyRes.ok) {
const { error } = await verifyRes.json();
// error.code === 'INVALID_CODE' or 'CODE_EXPIRED'
throw new Error(error.code);
}
const { data } = await verifyRes.json();
// data = { user_id: "external:usr_8821", core_user_id: "uuid", onboarding_stage: "financial_intake" }
// Step 4 β Create a session (SESSION_SECRET is your app's own concern, not Ozzie's)
return data;
}
SESSION_SECRET and session cookie management are handled by your own backend (e.g. Lovable). Ozzie does not issue session tokens β it only validates the OTP and returns the user_id and onboarding_stage your app needs to bootstrap the session.
For a complete reference on OTP request/verify parameters and error codes, see the Auth reference.
Multi-tenancy and client scopingβ
Your client_id is the root of your tenant namespace. Every user you create with POST /users belongs to your client, and your API credentials can only access users you created. You cannot read or modify users created by other API clients.
This means:
- You manage all your end users through a single set of credentials
- Users are identified by your own
external_user_idβ you own the mapping between Ozzie's user records and your database - All activity (financial intake, transactions, plans, chat) is scoped to users under your
client_id
There is no per-user authentication. Your server-side backend authenticates with Ozzie using your client credentials, then acts on behalf of your users. Never send your API credentials to a frontend client.
Security best practicesβ
Never expose credentials client-side. Your client_id and client_secret should only live in your server-side environment. A browser or mobile app that calls the Ozzie API directly would expose your credentials to anyone who inspects network traffic.
Use environment variables. Store credentials in environment variables, not hardcoded in source files. This prevents accidental commits to version control.
# .env (never commit this file)
OZZIE_CLIENT_ID=ozz_client_a1b2c3d4
OZZIE_CLIENT_SECRET=sk_live_xK9mP2qR7tL...
Rotate secrets periodically. Generate a new client_secret from the Ozzie dashboard on a regular schedule (e.g., every 90 days), or immediately if you suspect a secret has been compromised. Update your environment variables and deploy before revoking the old secret.
Use separate credentials per environment. Create distinct API clients for your development, staging, and production environments so that test traffic never touches real user data, and a compromised development secret cannot affect production.
Scope your server's outbound access. If your infrastructure supports it, restrict outbound HTTPS calls to api.ozzieapp.com from only the services that need it β not every server in your stack.