API Overview
Base URL: https://channel.ozzieapp.com/v1
The Channel API is the single integration point for all consumer apps. It is stateful β it owns users, OTP authentication, onboarding, chat history, and transactions.
The intelligence layer is available at api.ozzieapp.com but is an internal service. Consumer apps β including Lovable β do not call it directly. All requests go through channel.ozzieapp.com.
Endpointsβ
| Method | Path | Description |
|---|---|---|
POST | /v1/auth/otp/request | Request a 6-digit OTP code (WhatsApp or email) |
POST | /v1/auth/otp/verify | Verify OTP code β returns user_id + onboarding_stage |
POST | /v1/users | Create user identity |
GET | /v1/users/:user_id | Get user |
GET | /v1/users/:user_id/profile | Get channel user profile (onboarding_stage, plan_speed, etc.) |
PATCH | /v1/users/:user_id/onboarding/complete | Advance onboarding from plan β active |
POST | /v1/users/:user_id/financial-intake | Submit financial intake |
GET | /v1/users/:user_id/financial-intake | Get intake |
POST | /v1/users/:user_id/plan/generate | Generate plan β accepts personality_type in body |
GET | /v1/users/:user_id/plan | Get plan |
GET | /v1/users/:user_id/financial-profile | Envelope allocations β accepts ?personality_type= |
POST | /v1/users/:user_id/chat/completion | Stateless AI chat inference |
GET | /v1/users/:user_id/chat/messages | Retrieve chat history |
POST | /v1/transactions/parse | Stateless AI transaction parsing |
GET | /v1/personality/questions | Quiz question catalog (no user_id required) |
POST | /v1/personality/score | Score quiz answers and return personality type |
POST | /v1/money-moves/generate | Generate weekly move recommendations |
Authenticationβ
HTTP Bearer with a base64-encoded client_id:client_secret token.
Token format:
base64(client_id + ":" + client_secret)
Header:
Authorization: Bearer <base64_encoded_credentials>
Generating the token:
- Node.js
- Python
- curl
const token = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
// Authorization: `Bearer ${token}`
import base64
token = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
# f"Bearer {token}"
TOKEN=$(echo -n "your_client_id:your_client_secret" | base64)
curl -H "Authorization: Bearer $TOKEN" https://channel.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.
Request and response conventionsβ
Request headersβ
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer <token> |
Content-Type | Yes (POST/PATCH) | application/json |
Idempotency-Key | No | UUID v4 β for idempotent POST requests |
Response envelopeβ
{
"object": "object_type_name",
"data": { ... }
}
HTTP status codesβ
| Status | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource was created |
400 Bad Request | Malformed JSON or validation error |
401 Unauthorized | Missing or invalid credentials |
403 Forbidden | Valid credentials, insufficient permissions |
404 Not Found | Resource does not exist |
409 Conflict | Resource already exists |
422 Unprocessable Entity | Business logic error (e.g., intake missing) |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Server error |
Error codesβ
All errors follow this shape:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable explanation."
}
}
| Code | HTTP | Description |
|---|---|---|
INVALID_JSON | 400 | Request body is not valid JSON |
VALIDATION_ERROR | 400 | Field failed validation |
INVALID_CODE | 400 | OTP code is wrong or already used |
CODE_EXPIRED | 400 | OTP code has passed its 10-minute TTL |
UNAUTHORIZED | 401 | Missing or invalid credentials |
FORBIDDEN | 403 | Credentials valid but access denied |
NOT_FOUND | 404 | Resource does not exist |
CONFLICT | 409 | Unique constraint conflict (e.g., duplicate external_user_id) |
INTAKE_REQUIRED | 422 | Financial intake must be submitted first |
PLAN_REQUIRED | 422 | Plan must be generated first |
RATE_LIMIT_EXCEEDED | 429 | Rate limit exceeded β see Retry-After header |
INTERNAL_ERROR | 500 | Unexpected server error |
Rate limitingβ
Rate limits are applied per client_id. When exceeded, the API returns HTTP 429:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1746460800
| Tier | Requests per minute |
|---|---|
| Standard | 120 |
| Growth | 600 |
| Enterprise | Custom |
Always implement exponential backoff when handling 429 responses.