Plan
The Plan is the central output of Ozzie's financial analysis engine. It takes the user's financial intake data (income, expenses, debts, goals) and produces a structured roadmap with budget allocations, projected milestones, and prioritized action items.
The plan is the prerequisite for generating money moves and shapes all AI coach responses in chat.
Object referenceβ
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the plan |
user_id | string | The Ozzie user this plan belongs to |
timeline_months | integer | Projected months to reach the user's primary goal |
allocations | object | JSONB budget breakdown by category (percentages) |
allocations.needs | number | % of net income allocated to essential needs |
allocations.wants | number | % of net income allocated to discretionary spending |
allocations.savings | number | % of net income allocated to savings |
allocations.debt | number | % of net income allocated to debt payments |
key_metrics | object | Key projected financial indicators |
key_metrics.monthly_savings_amount | number | Dollar amount saved per month under this plan |
key_metrics.projected_goal_date | string (ISO 8601) | Estimated date the user reaches their goal |
key_metrics.current_savings_rate | number | Current savings rate as a percentage of income |
key_metrics.target_savings_rate | number | Recommended savings rate to reach the goal on time |
key_metrics.debt_payoff_date | string (ISO 8601) | null | Projected debt payoff date (if the goal is debt) |
action_items | string[] | Ordered list of recommended next actions |
personality_type | string | null | Applied personality profile (if personalized) |
created_at | string (ISO 8601) | When the plan was generated |
updated_at | string (ISO 8601) | When the plan was last updated |
POST /v1/users/{user_id}/planβ
Calculates a new financial plan for the user, or retrieves and updates an existing one. If a plan already exists, calling this endpoint recalculates it based on the most recent intake and transaction data.
This endpoint requires a financial intake to exist for the user. If no intake has been submitted, you will receive an INTAKE_REQUIRED error.
Plan generation is idempotent per user. You can safely call this endpoint multiple times β Ozzie will return the most up-to-date plan, recalculating if new intake or transaction data has been submitted since the last generation.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
Request bodyβ
No request body required. Ozzie reads all existing intake and transaction data for the user.
Requestβ
- curl
- Node.js
- Python
curl -X POST https://channel.ozzieapp.com/v1/users/usr_4f8a1b2c3d/plan \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu" \
-H "Content-Type: application/json"
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/users/usr_4f8a1b2c3d/plan',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
const { data } = await response.json();
console.log(data);
import base64, os, httpx
token = base64.b64encode(
f"{os.environ['OZZIE_CLIENT_ID']}:{os.environ['OZZIE_CLIENT_SECRET']}".encode()
).decode()
response = httpx.post(
"https://channel.ozzieapp.com/v1/users/usr_4f8a1b2c3d/plan",
headers={"Authorization": f"Bearer {token}"},
)
print(response.json()["data"])
Responseβ
{
"object": "plan",
"data": {
"id": "pln_8b3d1e9f2a",
"user_id": "usr_4f8a1b2c3d",
"timeline_months": 57,
"allocations": {
"needs": 60,
"wants": 20,
"savings": 15,
"debt": 5
},
"key_metrics": {
"monthly_savings_amount": 562.50,
"projected_goal_date": "2029-11-01T00:00:00Z",
"current_savings_rate": 8,
"target_savings_rate": 15,
"debt_payoff_date": null
},
"action_items": [
"Increase your savings contribution from 8% to 15% of net income β start with $562.50/month",
"Reduce discretionary spending (currently 28%) to 20% by cutting dining out and streaming subscriptions",
"Set up an automatic transfer on payday so savings happen before you spend",
"Review your food spending β you are $120/month above a reasonable baseline for your income",
"When your emergency fund reaches $3,000, consider opening a high-yield savings account"
],
"personality_type": null,
"created_at": "2025-05-05T14:50:00Z",
"updated_at": "2025-05-05T14:50:00Z"
}
}
Errorsβ
| Code | HTTP Status | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid credentials |
NOT_FOUND | 404 | The user does not exist |
INTAKE_REQUIRED | 422 | No financial intake exists for the user |
INTERNAL_ERROR | 500 | Plan calculation failed β safe to retry |
POST /v1/users/{user_id}/plan/personalizeβ
Adjusts the user's existing plan based on a personality profile. Personality-aware plans adjust the language, pacing, and emphasis of recommendations to match how the user thinks about money.
Personalization requires a plan to already exist. Call POST /plan first, then call /plan/personalize to add the personality profile.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
Request bodyβ
| Field | Type | Required | Description |
|---|---|---|---|
personality_type | string | Yes | The personality profile identifier (e.g., "optimizer", "avoider", "planner", "spender") |
Personality typesβ
| Type | Description |
|---|---|
"optimizer" | Data-driven, motivated by efficiency and maximum returns |
"planner" | Methodical, prefers detailed roadmaps and predictability |
"avoider" | Anxious about finances; needs reassurance and small wins |
"spender" | Lifestyle-oriented; needs strategies that do not feel like deprivation |
Requestβ
- curl
- Node.js
- Python
curl -X POST https://channel.ozzieapp.com/v1/users/usr_4f8a1b2c3d/plan/personalize \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu" \
-H "Content-Type: application/json" \
-d '{
"personality_type": "avoider"
}'
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/users/usr_4f8a1b2c3d/plan/personalize',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ personality_type: 'avoider' }),
}
);
const { data } = await response.json();
console.log(data.action_items);
import base64, os, httpx
token = base64.b64encode(
f"{os.environ['OZZIE_CLIENT_ID']}:{os.environ['OZZIE_CLIENT_SECRET']}".encode()
).decode()
response = httpx.post(
"https://channel.ozzieapp.com/v1/users/usr_4f8a1b2c3d/plan/personalize",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={"personality_type": "avoider"},
)
print(response.json()["data"]["action_items"])
Responseβ
Returns the updated plan with the personality profile applied. Notice how action_items shift to softer, smaller steps for an "avoider" profile.
{
"object": "plan",
"data": {
"id": "pln_8b3d1e9f2a",
"user_id": "usr_4f8a1b2c3d",
"timeline_months": 57,
"allocations": {
"needs": 60,
"wants": 20,
"savings": 15,
"debt": 5
},
"key_metrics": {
"monthly_savings_amount": 562.50,
"projected_goal_date": "2029-11-01T00:00:00Z",
"current_savings_rate": 8,
"target_savings_rate": 15,
"debt_payoff_date": null
},
"action_items": [
"You are already saving β that is something to feel good about. Let us just grow it a little.",
"Try transferring just $50 more per paycheck to savings this month. One small step.",
"You do not need to cut everything. Pick one expense this week that felt forgettable and pause it.",
"Set up an automatic transfer of $50 on payday β you will not notice it, but your savings will.",
"Your goal is reachable. At this pace you will have $3,000 saved in 6 months."
],
"personality_type": "avoider",
"created_at": "2025-05-05T14:50:00Z",
"updated_at": "2025-05-05T15:10:00Z"
}
}
Errorsβ
| Code | HTTP Status | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid credentials |
NOT_FOUND | 404 | The user does not exist |
PLAN_REQUIRED | 422 | No plan exists yet β call POST /plan first |
PERSONALITY_REQUIRED | 422 | personality_type field missing from the request body |
VALIDATION_ERROR | 422 | Unknown personality_type value |
INVALID_JSON | 400 | Request body is not valid JSON |
You can determine a user's personality type by running a short quiz in your onboarding flow and then passing the result here. The personality_type string is stored on the plan and reflected in all subsequent AI coach responses for that user.