External ID Integration
The Ozzie API supports two distinct integration patterns. You can choose whichever fits your architecture β or mix both within the same client account.
Two Integration Patternsβ
Flow A β Explicit user creationβ
You create a user record first by calling POST /v1/users, receive an Ozzie UUID (ozz_usr_...), and use that UUID in all subsequent requests.
POST /v1/users β returns id: "ozz_usr_abc123"
POST /v1/users/ozz_usr_abc123/financial-intake β uses Ozzie UUID
POST /v1/users/ozz_usr_abc123/plan/generate β uses Ozzie UUID
POST /v1/users/ozz_usr_abc123/chat/messages β uses Ozzie UUID
Best for: integrations where you want full control over user creation, or where you need to store the Ozzie UUID in your own database.
Flow B β External ID auto-provisioningβ
You skip user creation entirely. Use the external:{your_id} format as the {user_id} path parameter in any endpoint. On the first call, Ozzie automatically creates the user behind the scenes and continues processing the request.
POST /v1/users/external:usr_8821/financial-intake β user auto-created on first call
POST /v1/users/external:usr_8821/plan/generate β same user, no creation step needed
POST /v1/users/external:usr_8821/chat/messages β same user
Best for: integrations where you don't want to manage a separate user creation step, or where you're connecting an existing user base without migrating IDs.
There is no configuration or flag needed to use Flow B. Any non-UUID value prefixed with external: is automatically treated as an external ID lookup. If the user does not exist yet, they are created silently.
How external:{id} worksβ
When Ozzie receives a request with external:{your_id} as the user_id path parameter, it:
- Strips the
external:prefix and extracts your ID - Looks up the user by
(api_client_id, external_user_id) - If found β proceeds with the request using the matched user
- If not found β auto-creates a new user with
external_user_id = your_idandcreated_via = "api_external", then proceeds with the request
The auto-created user starts with onboarding_stage: "financial_intake". Ozzie advances the stage automatically after intake and plan generation, just as with users created via POST /v1/users.
Choosing between the two flowsβ
| Situation | Recommended pattern |
|---|---|
| You want to register users with name, email, and phone before they interact | Flow A β POST /v1/users |
| You have an existing user database and want zero migration friction | Flow B β external:{id} on any endpoint |
| You're building a WhatsApp bot or conversational interface | Flow A β set phone on creation for number matching |
| You're a fintech app proxying requests for your own users | Flow B β your database ID as the external ID |
| You need both options in the same integration | Mix freely β each user can be created either way |
Flow B β Full exampleβ
This example sends financial intake, generates a plan, and starts a chat β all without ever calling POST /v1/users:
- curl
- Node.js
- Python
# Step 1 β Send financial intake (user is auto-created on this call)
curl -X POST https://api.ozzieapp.com/v1/users/external:usr_8821/financial-intake \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"monthly_income": 5000,
"monthly_expenses": 3200,
"financial_goal": "savings"
}'
# Step 2 β Generate plan
curl -X POST https://api.ozzieapp.com/v1/users/external:usr_8821/plan/generate \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Step 3 β Chat
curl -X POST https://api.ozzieapp.com/v1/users/external:usr_8821/chat/messages \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "How much should I save this month?"}'
import fetch from 'node-fetch';
const BASE_URL = 'https://api.ozzieapp.com/v1';
const USER_REF = 'external:usr_8821'; // your own user ID, prefixed with "external:"
const TOKEN = Buffer.from('your_client_id:your_client_secret').toString('base64');
const headers = {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
};
async function post(path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method: 'POST', headers, body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(`[${json.error?.code}] ${json.error?.message}`);
return json.data;
}
// Step 1 β Financial intake (user is auto-created here if new)
const intake = await post(`/users/${USER_REF}/financial-intake`, {
monthly_income: 5000,
monthly_expenses: 3200,
financial_goal: 'savings',
});
console.log('Intake recorded. Surplus:', intake.monthly_surplus);
// Step 2 β Generate plan
const plan = await post(`/users/${USER_REF}/plan/generate`, {});
console.log('Plan tier:', plan.plan_tier);
// Step 3 β Chat
const reply = await post(`/users/${USER_REF}/chat/messages`, {
message: 'How much should I save this month?',
});
console.log('Ozzie:', reply.message);
import httpx, base64
BASE_URL = "https://api.ozzieapp.com/v1"
USER_REF = "external:usr_8821" # your own user ID, prefixed with "external:"
TOKEN = base64.b64encode(b"your_client_id:your_client_secret").decode()
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
# Step 1 β Financial intake (user is auto-created here if new)
resp = httpx.post(
f"{BASE_URL}/users/{USER_REF}/financial-intake",
headers=headers,
json={"monthly_income": 5000, "monthly_expenses": 3200, "financial_goal": "savings"},
)
resp.raise_for_status()
print("Surplus:", resp.json()["data"]["monthly_surplus"])
# Step 2 β Generate plan
resp = httpx.post(f"{BASE_URL}/users/{USER_REF}/plan/generate", headers=headers, json={})
resp.raise_for_status()
print("Plan tier:", resp.json()["data"]["plan_tier"])
# Step 3 β Chat
resp = httpx.post(
f"{BASE_URL}/users/{USER_REF}/chat/messages",
headers=headers,
json={"message": "How much should I save this month?"},
)
resp.raise_for_status()
print("Ozzie:", resp.json()["data"]["message"])
Retrieving an auto-provisioned userβ
Users created via Flow B can be retrieved at any time using GET /v1/users/external:{id} or by their Ozzie UUID once you know it:
curl https://api.ozzieapp.com/v1/users/external:usr_8821 \
-H "Authorization: Bearer YOUR_BEARER_TOKEN"
{
"object": "user",
"data": {
"id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"external_user_id": "usr_8821",
"name": null,
"email": null,
"phone": null,
"language": "en",
"onboarding_stage": "active",
"created_at": "2026-05-06T10:00:00Z"
}
}
Auto-provisioned users start with name, email, and phone as null. You can enrich the record later β contact commercial@ozzieapp.com if you need a user update endpoint.
Mixing both flowsβ
Both patterns are fully compatible within the same API client account. Some users can be created explicitly via POST /v1/users (e.g., users who sign up with name and email), while others are auto-provisioned via external:{id} (e.g., anonymous users or users imported from an existing system).
The only constraint is that external_user_id must be unique per API client. An explicit POST /v1/users with the same external_user_id as an auto-provisioned user will return a 409 CONFLICT.
Security considerationsβ
- The
external:prefix is resolved after authentication. Unauthenticated requests cannot use it. - Users auto-provisioned via Flow B are scoped to your API client. Another client cannot access them even if they know the
external_user_id. - Rate limits apply equally to both flows. Auto-provisioning a new user does not consume an extra API call β it is part of the same request.