Plan
The Plan is the core output of Ozzie's financial analysis engine. It takes a 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 every response from the AI chat coach.
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 repayment |
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 hit the goal on time |
key_metrics.debt_payoff_date | string (ISO 8601) | null | Projected debt-free date (if goal is debt) |
action_items | string[] | Ordered list of recommended next actions |
personality_type | string | null | Personality profile applied (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 refreshes an existing one. If a plan already exists, calling this endpoint recalculates it based on the latest intake and transaction data.
This endpoint requires that a financial intake exists 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 current 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 data from the user's existing intake and transactions.
Requestβ
- curl
- Node.js
- Python
curl -X POST https://api.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://api.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://api.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",
"Cut discretionary spending (currently 28%) down to 20% by reducing dining out and streaming subscriptions",
"Set up an automatic transfer on payday so savings happen before you can spend them",
"Review your grocery spending β you're currently $120/month over a reasonable baseline for your income",
"Once your emergency fund hits $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 tune the language, pacing, and emphasis of recommendations to match how the user thinks about money.
Personality personalization requires a plan to already exist. Call POST /plan first, then call /plan/personalize to layer the personality profile on top.
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 don't feel like deprivation |
Requestβ
- curl
- Node.js
- Python
curl -X POST https://api.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://api.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://api.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. Note how the action_items shift to gentler, 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're already saving β that's something to feel good about. Let's just grow it a little.",
"Try moving just $50 more per paycheck into savings this month. One small step.",
"You don't have to cut everything. Pick one expense this week that felt forgettable and pause it.",
"Set up a $50 automatic transfer on payday β you won't notice it, but your savings will.",
"Your goal is doable. At this pace you'll have $3,000 saved within 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 is 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, 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.