API Overview
Base URL & Versioningβ
The Ozzie API is versioned via the URL path. The current stable version is v1.
https://api.ozzieapp.com/v1
Versioning policy:
- The version prefix (
/v1,/v2, etc.) is incremented only for breaking changes. - Additive changes (new optional fields, new endpoints) are made without a version bump.
- Deprecated versions are announced at least 90 days before sunset and will return a
Deprecationresponse header during the sunset window. - Always pin to an explicit version in your base URL. Never use
/latest.
When a version enters its deprecation window, all responses will include:
Deprecation: true
Sunset: Sat, 01 Nov 2025 00:00:00 GMT
All Available Endpointsβ
| Method | Path | Description |
|---|---|---|
POST | /v1/users | Create a new user |
GET | /v1/users/{user_id} | Retrieve a user by Ozzie ID or external_user_id |
PATCH | /v1/users/{user_id} | Update user fields (name, email, phone, language) |
POST | /v1/users/{user_id}/financial-intake | Submit a monthly financial snapshot |
GET | /v1/users/{user_id}/financial-intake | Get the most recent financial intake |
GET | /v1/users/{user_id}/financial-intake/history | Get all historical intake snapshots |
POST | /v1/users/{user_id}/plan/generate | Generate a personalized financial plan |
GET | /v1/users/{user_id}/plan | Get the current active plan |
POST | /v1/users/{user_id}/goals | Create a financial goal |
GET | /v1/users/{user_id}/goals | List all goals |
GET | /v1/users/{user_id}/goals/{goal_id} | Get a specific goal |
PATCH | /v1/users/{user_id}/goals/{goal_id} | Update a goal |
DELETE | /v1/users/{user_id}/goals/{goal_id} | Delete a goal |
POST | /v1/users/{user_id}/transactions | Submit transactions (text, image, PDF, spreadsheet) |
GET | /v1/users/{user_id}/transactions | List transactions with filters |
GET | /v1/users/{user_id}/transactions/{transaction_id} | Get a specific transaction |
DELETE | /v1/users/{user_id}/transactions/{transaction_id} | Delete a transaction |
GET | /v1/users/{user_id}/money-moves | Get personalized spending recommendations |
POST | /v1/users/{user_id}/chat | Send a message to Ozzie's financial assistant |
GET | /v1/users/{user_id}/chat/history | Get chat message history |
Authenticationβ
All API requests must include an Authorization header using HTTP Bearer authentication. The token is the Base64 encoding of your client_id and client_secret joined by a colon.
Token format:
base64(client_id + ":" + client_secret)
Header format:
Authorization: Bearer <base64_encoded_credentials>
Generating the token:
- Node.js
- Python
- curl
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
// Use as: `Bearer ${token}`
import base64
token = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
# Use as: f"Bearer {token}"
TOKEN=$(echo -n "your_client_id:your_client_secret" | base64)
curl -H "Authorization: Bearer $TOKEN" https://api.ozzieapp.com/v1/users
Never expose your client_secret in client-side code, mobile apps, or public repositories. All API calls must originate from your backend server.
Request & Response Conventionsβ
Request Headersβ
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer <token> |
Content-Type | Yes (for POST/PATCH) | application/json |
Accept | No (recommended) | application/json |
Idempotency-Key | No | UUID v4 string for idempotent POST requests |
Response Envelopeβ
All successful responses follow this envelope structure:
{
"object": "object_type_name",
"data": { ... }
}
For list endpoints:
{
"object": "list",
"data": {
"items": [ ... ],
"has_more": true,
"next_cursor": "2025-04-30T12:00:00Z",
"total_count": 142
}
}
HTTP Status Codesβ
| Status | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource was created |
204 No Content | Resource was deleted |
400 Bad Request | Malformed JSON or validation error |
401 Unauthorized | Missing or invalid credentials |
403 Forbidden | Valid credentials, but insufficient permissions |
404 Not Found | Resource does not exist |
409 Conflict | Resource already exists (e.g., duplicate user) |
422 Unprocessable Entity | Business logic error (e.g., intake missing) |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Ozzie server error |
Paginationβ
List endpoints use cursor-based pagination with ISO datetime cursors. This approach is stable even when new records are inserted between pages.
Query Parametersβ
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of records per page (max 200) |
cursor | string | β | ISO 8601 datetime string β returns records older than this timestamp |
from | string | β | Filter: include records on or after this date (YYYY-MM-DD) |
to | string | β | Filter: include records on or before this date (YYYY-MM-DD) |
Paginated Response Fieldsβ
| Field | Type | Description |
|---|---|---|
has_more | boolean | true if additional pages exist |
next_cursor | string | null | Pass this as cursor on the next request to get the next page |
total_count | integer | Total number of matching records (not just this page) |
Pagination Exampleβ
async function fetchAllTransactions(userId) {
const allTransactions = [];
let cursor = null;
do {
const params = new URLSearchParams({ limit: '200' });
if (cursor) params.set('cursor', cursor);
const res = await fetch(
`https://api.ozzieapp.com/v1/users/${userId}/transactions?${params}`,
{ headers }
);
const json = await res.json();
allTransactions.push(...json.data.transactions);
cursor = json.data.has_more ? json.data.next_cursor : null;
} while (cursor);
return allTransactions;
}
Cursors are based on created_at timestamps. Records created after you start paginating will not appear in the current page sequence β start a fresh request to pick up new records.
Error Codesβ
All errors follow this shape:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable explanation of what went wrong."
}
}
| Code | HTTP Status | Description |
|---|---|---|
INVALID_JSON | 400 | The request body is not valid JSON. Check for trailing commas, missing quotes, or encoding issues. |
VALIDATION_ERROR | 400 | One or more fields failed validation. The message will identify the specific field and constraint. |
UNAUTHORIZED | 401 | The Authorization header is missing, malformed, or the credentials are invalid. |
FORBIDDEN | 403 | The credentials are valid but do not have permission to access this resource (e.g., accessing another client's user). |
NOT_FOUND | 404 | The requested resource (user, transaction, goal, etc.) does not exist. |
CONFLICT | 409 | A resource with a conflicting unique key already exists (e.g., external_user_id already in use). |
INTAKE_REQUIRED | 422 | A financial intake snapshot must be submitted before this operation (e.g., plan generation). |
PLAN_REQUIRED | 422 | A financial plan must be generated before this operation (e.g., money moves). |
PERSONALITY_REQUIRED | 422 | Ozzie's personality profile for this user has not been initialized yet. This resolves automatically after plan generation. |
RATE_LIMIT_EXCEEDED | 429 | You have exceeded the request rate limit. See the Retry-After header for when to retry. |
VERSION_MISMATCH | 400 | The API version you are targeting is no longer supported. Update your base URL to the current version. |
INTERNAL_ERROR | 500 | An unexpected error occurred on Ozzie's servers. These are automatically logged and investigated. Retry with exponential backoff. |
Rate Limitingβ
Rate limits are applied per client_id. Current limits:
| Tier | Requests per minute | Burst |
|---|---|---|
| Standard | 120 | 20 |
| Growth | 600 | 100 |
| Enterprise | Custom | Custom |
When the limit is exceeded, the API returns HTTP 429 with:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1746460800
| Response Header | Description |
|---|---|
Retry-After | Seconds to wait before retrying |
X-RateLimit-Limit | Your total requests-per-minute allowance |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Always implement exponential backoff when handling 429 responses. A simple retry loop without delay will worsen rate limit pressure. Start with a 1-second delay, doubling up to 32 seconds.
Idempotencyβ
POST requests that create resources support idempotency via the Idempotency-Key header. If you supply the same key on a repeated request (e.g., after a network timeout), Ozzie will return the original response instead of creating a duplicate.
POST https://api.ozzieapp.com/v1/users
Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ=
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"external_user_id": "usr_8821",
"name": "Maria Santos"
}
Rules:
- The key must be a UUID v4.
- Keys are scoped to your
client_idand expire after 24 hours. - If the original request is still in flight, the retry will receive HTTP
409with codeCONFLICTuntil the original completes. - Supported on:
POST /v1/users,POST /v1/users/{user_id}/financial-intake,POST /v1/users/{user_id}/transactions.
POST /v1/users/{user_id}/plan/generate and POST /v1/users/{user_id}/chat are not idempotent by design β each call intentionally generates a new resource.