Financial Intake
The financial intake is the foundation of every Ozzie user experience. It captures a point-in-time snapshot of a user's monthly financial picture β income, expenses, and primary goal. Ozzie uses this data to generate a personalized financial plan, compute savings targets, and provide context-aware recommendations.
POST /v1/users/{user_id}/plan/generate will return INTAKE_REQUIRED until at least one financial intake snapshot has been submitted. Always submit an intake before generating a plan.
The Financial Intake Objectβ
{
"object": "financial_intake",
"data": {
"id": "ozz_intake_01HX9M3FQKRNT8YDWP6BZ2LSA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"monthly_income": 5000,
"monthly_expenses": 3200,
"monthly_surplus": 1800,
"financial_goal": "savings",
"next_pay_date": "2025-05-15",
"created_at": "2025-05-05T14:31:00Z"
}
}
Financial Intake Object Fieldsβ
| Field | Type | Description |
|---|---|---|
id | string | Ozzie-generated UUID for this intake snapshot |
user_id | string | The Ozzie user this intake belongs to |
monthly_income | number | Gross monthly income in the user's base currency |
monthly_expenses | number | Total monthly outgoings |
monthly_surplus | number | Computed field: monthly_income - monthly_expenses |
financial_goal | string | "savings" or "debt" β the user's primary goal type |
next_pay_date | string | null | User's next paycheck date in YYYY-MM-DD format |
created_at | string | ISO 8601 UTC timestamp of when this snapshot was created |
Immutable Snapshot Modelβ
Each call to POST /v1/users/{user_id}/financial-intake creates a new immutable snapshot. Previous snapshots are preserved in history. Ozzie always uses the most recently created snapshot for plan generation and recommendations.
This design means:
- You never need to "update" an intake β just submit a new one.
- Historical snapshots are available via
GET /v1/users/{user_id}/financial-intake/historyfor audit and trend analysis. - Re-generating a plan after a new intake submission will pick up the latest numbers automatically.
May 1 intake: income=4500, expenses=3100 β used for May plan
β
May 31 intake: income=5000, expenses=3200 β becomes the new latest
β
POST /plan/generate β uses May 31 snapshot
Submit a fresh intake when the user reports a meaningful change: a salary increase, a new recurring bill, paying off a debt, or a life event (new job, moved cities). Monthly re-submission is a good default cadence.
POST /v1/users/{user_id}/financial-intakeβ
Stores a new monthly financial snapshot for the user.
Path Parametersβ
| Parameter | Description |
|---|---|
user_id | The Ozzie user UUID or external:{external_user_id} |
Request Bodyβ
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
monthly_income | number | Yes | Must be β₯ 0 | Gross monthly income in the user's base currency (e.g., USD) |
monthly_expenses | number | Yes | Must be β₯ 0 | Total monthly outgoings (rent, food, bills, subscriptions, etc.) |
financial_goal | string | Yes | "savings" | "debt" | The user's primary financial objective |
next_pay_date | string | No | ISO date YYYY-MM-DD | Date of the user's next paycheck β used for cash-flow timing |
financial_goal Valuesβ
| Value | Meaning | Plan behavior |
|---|---|---|
"savings" | User's priority is building savings or an emergency fund | Plan emphasizes contribution rate and compounding growth |
"debt" | User's priority is paying down debt | Plan emphasizes debt payoff sequencing (avalanche or snowball) |
Responseβ
Returns the created financial_intake object with HTTP 201 Created.
Errorsβ
| Code | HTTP | When |
|---|---|---|
NOT_FOUND | 404 | The user_id does not exist |
VALIDATION_ERROR | 400 | A required field is missing or has an invalid value |
UNAUTHORIZED | 401 | Missing or invalid credentials |
Examplesβ
- curl
- Node.js
- Python
curl -X POST \
https://api.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/financial-intake \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ=" \
-H "Content-Type: application/json" \
-d '{
"monthly_income": 5000,
"monthly_expenses": 3200,
"financial_goal": "savings",
"next_pay_date": "2025-05-15"
}'
import fetch from 'node-fetch';
const token = Buffer.from('your_client_id:your_client_secret').toString('base64');
const userId = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE';
const response = await fetch(
`https://api.ozzieapp.com/v1/users/${userId}/financial-intake`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
monthly_income: 5000,
monthly_expenses: 3200,
financial_goal: 'savings',
next_pay_date: '2025-05-15',
}),
}
);
const { data: intake } = await response.json();
console.log(`Monthly surplus: $${intake.monthly_surplus}`);
import requests
import base64
token = base64.b64encode(b'your_client_id:your_client_secret').decode()
user_id = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE'
response = requests.post(
f'https://api.ozzieapp.com/v1/users/{user_id}/financial-intake',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
},
json={
'monthly_income': 5000,
'monthly_expenses': 3200,
'financial_goal': 'savings',
'next_pay_date': '2025-05-15',
}
)
intake = response.json()['data']
print(f"Monthly surplus: ${intake['monthly_surplus']}")
Example Response (201 Created):
{
"object": "financial_intake",
"data": {
"id": "ozz_intake_01HX9M3FQKRNT8YDWP6BZ2LSA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"monthly_income": 5000,
"monthly_expenses": 3200,
"monthly_surplus": 1800,
"financial_goal": "savings",
"next_pay_date": "2025-05-15",
"created_at": "2025-05-05T14:31:00Z"
}
}
Error Response β Missing required field (400):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Field 'financial_goal' is required. Must be one of: 'savings', 'debt'."
}
}
Error Response β Invalid income value (400):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Field 'monthly_income' must be a non-negative number. Received: -500."
}
}
GET /v1/users/{user_id}/financial-intakeβ
Returns the most recently created financial intake snapshot for the user.
Path Parametersβ
| Parameter | Description |
|---|---|
user_id | The Ozzie user UUID or external:{external_user_id} |
Responseβ
Returns the financial_intake object with HTTP 200 OK. If no intake has been submitted, returns HTTP 404 with code NOT_FOUND.
Errorsβ
| Code | HTTP | When |
|---|---|---|
NOT_FOUND | 404 | The user exists but has not submitted any intake yet, or the user_id is invalid |
UNAUTHORIZED | 401 | Missing or invalid credentials |
Examplesβ
- curl
- Node.js
- Python
curl https://api.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/financial-intake \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ="
import fetch from 'node-fetch';
const token = Buffer.from('your_client_id:your_client_secret').toString('base64');
const userId = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE';
const response = await fetch(
`https://api.ozzieapp.com/v1/users/${userId}/financial-intake`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (response.status === 404) {
console.log('No intake submitted yet. Prompt the user for their financial details.');
} else {
const { data: intake } = await response.json();
console.log(`Latest intake from ${intake.created_at}: surplus $${intake.monthly_surplus}`);
}
import requests
import base64
token = base64.b64encode(b'your_client_id:your_client_secret').decode()
user_id = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE'
response = requests.get(
f'https://api.ozzieapp.com/v1/users/{user_id}/financial-intake',
headers={'Authorization': f'Bearer {token}'}
)
if response.status_code == 404:
print('No intake submitted yet.')
else:
intake = response.json()['data']
print(f"Latest intake: surplus ${intake['monthly_surplus']}")
Example Response (200 OK):
{
"object": "financial_intake",
"data": {
"id": "ozz_intake_01HX9M3FQKRNT8YDWP6BZ2LSA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"monthly_income": 5000,
"monthly_expenses": 3200,
"monthly_surplus": 1800,
"financial_goal": "savings",
"next_pay_date": "2025-05-15",
"created_at": "2025-05-05T14:31:00Z"
}
}
Error Response β No intake yet (404):
{
"error": {
"code": "NOT_FOUND",
"message": "No financial intake found for user 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE'. Submit a POST /v1/users/{user_id}/financial-intake first."
}
}
Before calling POST /v1/users/{user_id}/plan/generate, you can optionally call this endpoint to confirm an intake exists and preview the monthly_surplus. This avoids a round-trip error if the intake was never submitted.