Money Moves
Money Moves are structured, time-boxed financial action cycles delivered to users on their chosen cadence (weekly, biweekly, etc.). Each cycle contains a set of tasks across three categories:
| Task type | Description |
|---|---|
"do" | A concrete financial action with an associated dollar amount (e.g., transfer $150 to savings) |
"learn" | A short financial education task tailored to the user's situation |
"mind" | A mindset or reflection exercise to build healthy money habits |
Money Moves require a financial plan to exist first. Use POST /v1/users/{user_id}/plan before calling /money-moves/generate.
Object referenceβ
Cycle objectβ
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the cycle |
user_id | string | The Ozzie user this cycle belongs to |
due_date | string (ISO 8601) | The date by which this cycle's tasks should be completed |
cadence | "weekly" | "biweekly" | "twice_monthly" | "monthly" | The frequency this cycle is part of |
status | "scheduled" | "available" | "completed" | "skipped" | Current state of the cycle |
tasks | Task[] | The list of tasks in this cycle |
created_at | string (ISO 8601) | When the cycle was generated |
Task objectβ
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the task |
task_type | "do" | "learn" | "mind" | The category of the task |
title | string | Human-readable description of the task |
amount_cents | integer | null | Dollar amount in cents (only present for "do" tasks) |
status | "pending" | "done" | "skipped" | Whether the task has been completed |
completed_at | string (ISO 8601) | null | When the task was marked done (null if pending/skipped) |
GET /v1/users/{user_id}/money-movesβ
Lists all money move cycles for a user. Supports filtering by status and cursor-based pagination.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
Query parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
status | "scheduled" | "available" | "completed" | "skipped" | No | Filter cycles by status |
limit | integer | No | Number of cycles to return. Default: 10. Max: 50. |
cursor | string | No | Pagination cursor from a previous response's next_cursor |
Requestβ
- curl
- Node.js
- Python
curl "https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves?status=available&limit=5" \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu"
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const params = new URLSearchParams({ status: 'available', limit: '5' });
const response = await fetch(
`https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves?${params}`,
{
headers: { 'Authorization': `Bearer ${token}` },
}
);
const { data, pagination } = 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.get(
"https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves",
params={"status": "available", "limit": 5},
headers={"Authorization": f"Bearer {token}"},
)
result = response.json()
print(result["data"])
Responseβ
{
"object": "list",
"data": [
{
"id": "mmv_3a7c9e1b2d",
"user_id": "usr_4f8a1b2c3d",
"due_date": "2025-05-19T23:59:59Z",
"cadence": "biweekly",
"status": "available",
"created_at": "2025-05-05T14:40:00Z",
"tasks": [
{
"id": "tsk_1a2b3c4d5e",
"task_type": "do",
"title": "Transfer $150 to your Emergency Fund savings account",
"amount_cents": 15000,
"status": "pending",
"completed_at": null
},
{
"id": "tsk_2b3c4d5e6f",
"task_type": "learn",
"title": "Read: Why a 3-month emergency fund is the right first milestone",
"amount_cents": null,
"status": "pending",
"completed_at": null
},
{
"id": "tsk_3c4d5e6f7g",
"task_type": "mind",
"title": "Write down one thing you'd feel less stressed about with $10,000 in savings",
"amount_cents": null,
"status": "pending",
"completed_at": null
}
]
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}
Errorsβ
| Code | HTTP Status | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid credentials |
NOT_FOUND | 404 | The user does not exist |
VALIDATION_ERROR | 422 | Invalid status value or limit out of range |
POST /v1/users/{user_id}/money-moves/generateβ
Triggers generation of the next money move cycle for the user. Ozzie's AI analyzes the user's current plan, goal, and transaction history to produce a new set of personalized tasks.
This endpoint requires that a financial plan already exists for the user. Call POST /v1/users/{user_id}/plan first. If no plan exists, you will receive a PLAN_REQUIRED error.
Each call generates exactly one new cycle. The cycle is placed in "scheduled" status and becomes "available" on its due_date.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
Request bodyβ
No request body required.
Requestβ
- curl
- Node.js
- Python
curl -X POST https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves/generate \
-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/money-moves/generate',
{
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/money-moves/generate",
headers={"Authorization": f"Bearer {token}"},
)
print(response.json()["data"])
Responseβ
Returns the newly generated cycle with all its tasks.
{
"object": "money_move",
"data": {
"id": "mmv_5d8f2a4b9c",
"user_id": "usr_4f8a1b2c3d",
"due_date": "2025-06-02T23:59:59Z",
"cadence": "biweekly",
"status": "scheduled",
"created_at": "2025-05-05T14:45:00Z",
"tasks": [
{
"id": "tsk_4d5e6f7g8h",
"task_type": "do",
"title": "Transfer $150 to your Emergency Fund β you're 12% of the way there!",
"amount_cents": 15000,
"status": "pending",
"completed_at": null
},
{
"id": "tsk_5e6f7g8h9i",
"task_type": "learn",
"title": "Read: How to automate your savings so you never forget a transfer",
"amount_cents": null,
"status": "pending",
"completed_at": null
},
{
"id": "tsk_6f7g8h9i0j",
"task_type": "mind",
"title": "Reflect: What's one subscription you've kept but barely used this month?",
"amount_cents": null,
"status": "pending",
"completed_at": null
}
]
}
}
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 financial plan exists for the user |
INTERNAL_ERROR | 500 | AI generation failed β safe to retry |
GET /v1/users/{user_id}/money-moves/{move_id}β
Retrieves a single money move cycle by ID, including all of its tasks.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
move_id | string | Yes | The money move cycle ID |
Requestβ
- curl
- Node.js
- Python
curl https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves/mmv_3a7c9e1b2d \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu"
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/money-moves/mmv_3a7c9e1b2d',
{
headers: { 'Authorization': `Bearer ${token}` },
}
);
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.get(
"https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves/mmv_3a7c9e1b2d",
headers={"Authorization": f"Bearer {token}"},
)
print(response.json()["data"])
Responseβ
{
"object": "money_move",
"data": {
"id": "mmv_3a7c9e1b2d",
"user_id": "usr_4f8a1b2c3d",
"due_date": "2025-05-19T23:59:59Z",
"cadence": "biweekly",
"status": "available",
"created_at": "2025-05-05T14:40:00Z",
"tasks": [
{
"id": "tsk_1a2b3c4d5e",
"task_type": "do",
"title": "Transfer $150 to your Emergency Fund savings account",
"amount_cents": 15000,
"status": "done",
"completed_at": "2025-05-06T09:15:00Z"
},
{
"id": "tsk_2b3c4d5e6f",
"task_type": "learn",
"title": "Read: Why a 3-month emergency fund is the right first milestone",
"amount_cents": null,
"status": "pending",
"completed_at": null
},
{
"id": "tsk_3c4d5e6f7g",
"task_type": "mind",
"title": "Write down one thing you'd feel less stressed about with $10,000 in savings",
"amount_cents": null,
"status": "pending",
"completed_at": null
}
]
}
}
Errorsβ
| Code | HTTP Status | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid credentials |
NOT_FOUND | 404 | User or money move cycle not found |
PATCH /v1/users/{user_id}/money-moves/{move_id}β
Updates the status of a money move cycle. Use this to mark a cycle as completed or skipped when the user finishes their tasks or opts out of the current cycle.
Path parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The Ozzie user ID |
move_id | string | Yes | The money move cycle ID |
Request bodyβ
| Field | Type | Required | Description |
|---|---|---|---|
status | "completed" | "skipped" | Yes | The new status for the cycle |
Requestβ
- curl
- Node.js
- Python
curl -X PATCH https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves/mmv_3a7c9e1b2d \
-H "Authorization: Bearer ozp_Y2xpZW50X2ExYjJjM2Q0OnNrX2xpdmVfeEs5bVAycVI3dEwu" \
-H "Content-Type: application/json" \
-d '{
"status": "completed"
}'
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/money-moves/mmv_3a7c9e1b2d',
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: 'completed' }),
}
);
const { data } = await response.json();
console.log(data.status); // "completed"
import base64, os, httpx
token = base64.b64encode(
f"{os.environ['OZZIE_CLIENT_ID']}:{os.environ['OZZIE_CLIENT_SECRET']}".encode()
).decode()
response = httpx.patch(
"https://api.ozzieapp.com/v1/users/usr_4f8a1b2c3d/money-moves/mmv_3a7c9e1b2d",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={"status": "completed"},
)
print(response.json()["data"]["status"]) # "completed"
Responseβ
Returns the updated cycle object.
{
"object": "money_move",
"data": {
"id": "mmv_3a7c9e1b2d",
"user_id": "usr_4f8a1b2c3d",
"due_date": "2025-05-19T23:59:59Z",
"cadence": "biweekly",
"status": "completed",
"created_at": "2025-05-05T14:40:00Z",
"tasks": [
{
"id": "tsk_1a2b3c4d5e",
"task_type": "do",
"title": "Transfer $150 to your Emergency Fund savings account",
"amount_cents": 15000,
"status": "done",
"completed_at": "2025-05-06T09:15:00Z"
},
{
"id": "tsk_2b3c4d5e6f",
"task_type": "learn",
"title": "Read: Why a 3-month emergency fund is the right first milestone",
"amount_cents": null,
"status": "done",
"completed_at": "2025-05-07T20:30:00Z"
},
{
"id": "tsk_3c4d5e6f7g",
"task_type": "mind",
"title": "Write down one thing you'd feel less stressed about with $10,000 in savings",
"amount_cents": null,
"status": "done",
"completed_at": "2025-05-07T20:35:00Z"
}
]
}
}
Errorsβ
| Code | HTTP Status | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid credentials |
NOT_FOUND | 404 | User or money move cycle not found |
VALIDATION_ERROR | 422 | Invalid status value, or cycle is already in a terminal state |
INVALID_JSON | 400 | Request body is not valid JSON |
After marking a cycle "completed" or "skipped", you can immediately call POST /money-moves/generate to queue the next cycle. This keeps your users' task lists fresh without gaps.