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/completion β 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/completion β 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
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 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, scores personality, 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 β Score personality
curl -X POST https://api.ozzieapp.com/v1/personality/score \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "answers": [{ "question_id": "q_001", "option_id": "q_001_b" }] }'
# Step 3 β 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 '{ "personality_type": "STOCKPILER" }'
# Step 4 β Chat
curl -X POST https://api.ozzieapp.com/v1/users/external:usr_8821/chat/completion \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "How much should I save this month?",
"personality_type": "STOCKPILER"
}'
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 β Score personality
const score = await post('/personality/score', {
answers: [{ question_id: 'q_001', option_id: 'q_001_b' }],
});
const personalityType = score.personality_type;
console.log('Personality:', personalityType);
// Step 3 β Generate plan
const plan = await post(`/users/${USER_REF}/plan/generate`, {
personality_type: personalityType,
});
console.log('Plan tier:', plan.plan_tier);
// Step 4 β Chat
const reply = await post(`/users/${USER_REF}/chat/completion`, {
message: 'How much should I save this month?',
personality_type: personalityType,
});
console.log('Ozzie:', reply.content);
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 β Score personality
resp = httpx.post(
f"{BASE_URL}/personality/score",
headers=headers,
json={"answers": [{"question_id": "q_001", "option_id": "q_001_b"}]},
)
resp.raise_for_status()
personality_type = resp.json()["data"]["personality_type"]
print("Personality:", personality_type)
# Step 3 β Generate plan
resp = httpx.post(
f"{BASE_URL}/users/{USER_REF}/plan/generate",
headers=headers,
json={"personality_type": personality_type},
)
resp.raise_for_status()
print("Plan tier:", resp.json()["data"]["plan_tier"])
# Step 4 β Chat
resp = httpx.post(
f"{BASE_URL}/users/{USER_REF}/chat/completion",
headers=headers,
json={"message": "How much should I save this month?", "personality_type": personality_type},
)
resp.raise_for_status()
print("Ozzie:", resp.json()["data"]["content"])
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,
"language": "en",
"created_at": "2026-05-06T10:00:00Z"
}
}
Auto-provisioned users start with name and email as null.
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}.
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.