Transactions
Transactions are the core data stream that powers Ozzie's spending analysis, plan tracking, and personalized recommendations. Ozzie uses GPT-4o to parse natural language descriptions, receipt images (OCR), bank statement PDFs, and CSV spreadsheets into structured, categorized transaction records.
A single submission can result in multiple transactions β for example, a text message saying "coffee $5 and lunch $15" will produce two separate transaction objects.
The Transaction Objectβ
{
"id": "ozz_txn_01HX9Q5NRWBF3KMZP4VC7YDLA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"amount_cents": 4500,
"currency": "USD",
"category": "food",
"envelope": "Daily Living",
"description": "Grocery shopping at Extra Supermarket",
"transaction_date": "2025-05-05",
"source": "api_text",
"ai_confidence": 0.97,
"created_at": "2025-05-05T14:34:00Z"
}
Transaction Object Fieldsβ
| Field | Type | Description |
|---|---|---|
id | string | Ozzie-generated UUID for this transaction |
user_id | string | The Ozzie user this transaction belongs to |
amount_cents | integer | Transaction amount in cents (e.g., 4500 = $45.00) |
currency | string | ISO 4217 currency code (e.g., "USD", "BRL", "EUR") |
category | string | Spending category (see table below) |
envelope | string | Budget envelope this transaction belongs to (see table below) |
description | string | AI-generated human-readable description of the transaction |
transaction_date | string | Transaction date in YYYY-MM-DD format |
source | string | How the transaction was submitted (see table below) |
ai_confidence | float | AI parsing confidence score from 0.0 to 1.0 |
created_at | string | ISO 8601 UTC timestamp of when the record was created in Ozzie |
category Valuesβ
| Value | Description |
|---|---|
food | Grocery stores, restaurants, cafes, food delivery |
transport | Gas, public transit, rideshare apps, parking, tolls |
housing | Rent, mortgage, home insurance, renovations |
utilities | Electricity, water, internet, phone bill |
health | Pharmacy, medical appointments, gym, health insurance |
entertainment | Streaming, cinema, games, events |
education | Tuition, books, courses, educational subscriptions |
clothing | Clothes, shoes, accessories |
income | Salary, freelance payment, extra income, reimbursements |
other | Anything that does not fit the categories above |
envelope Valuesβ
Ozzie groups each transaction into one of six budget envelopes. The envelope is derived automatically from the category and is always present in the response.
| Envelope | Included categories |
|---|---|
Bills | housing, utilities β fixed monthly bills |
Daily Living | food, transport β everyday spending |
Health & Wellness | health β health, pharmacy, gym |
Lifestyle | entertainment, education, clothing β lifestyle |
People | other β gifts, family, social events |
Your Future | income β salary, extra income, savings |
Use the envelope field to build spending summaries grouped by area of spending without any extra mapping on the client side.
source Valuesβ
| Value | Description |
|---|---|
whatsapp_text | Text message received via WhatsApp |
whatsapp_image | Image (receipt/screenshot) received via WhatsApp |
api_text | Text submitted via REST API |
api_image | Image submitted via REST API (base64) |
api_pdf | Text extracted from PDF submitted via REST API |
api_spreadsheet | CSV data submitted via REST API |
POST /v1/users/{user_id}/transactionsβ
Submit one or more transactions for parsing. Ozzie uses GPT-4o to extract structured transaction data from the provided content. The type field determines how the content is interpreted.
Path Parametersβ
| Parameter | Description |
|---|---|
user_id | The Ozzie UUID of the user or external:{external_user_id} |
Request Body β Discriminated Union by typeβ
The shape of the request body depends on the type field. All types share the language field.
type: "text"β
Submit a natural language description of one or more transactions.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "text" |
content | string | Yes | Natural language transaction description |
language | string | No | Language hint for parsing: "en" | "pt" | "es". Default: user's language. |
{
"type": "text",
"content": "Spent $45 at the grocery store and $12 on the subway",
"language": "en"
}
type: "image"β
Submit a receipt, screenshot, or purchase photo as a base64-encoded image. Ozzie performs OCR and analyzes the result.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "image" |
content | string | Yes | Base64-encoded image data |
mime_type | string | Yes | Image MIME type: "image/jpeg" | "image/png" | "image/webp" |
language | string | No | Language hint for OCR and parsing |
{
"type": "image",
"content": "/9j/4AAQSkZJRgABAQEASABIAAD...",
"mime_type": "image/jpeg",
"language": "en"
}
type: "pdf"β
Submit the extracted text content from a bank statement or PDF receipt. Extract the text from the PDF on your side before submitting.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "pdf" |
content | string | Yes | Plain text extracted from the PDF |
language | string | No | Language hint for parsing |
{
"type": "pdf",
"content": "BANK STATEMENT\nDate: 2025-05-01\nMerchant: Amazon.com\nAmount: -$89.99\nDate: 2025-05-02\nMerchant: Netflix\nAmount: -$15.99",
"language": "en"
}
type: "spreadsheet"β
Submit CSV-formatted transaction data. Include a header row. Ozzie normalizes column names flexibly (e.g., Date / date / transaction_date are all recognized).
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "spreadsheet" |
content | string | Yes | CSV string with header row |
language | string | No | Language hint for parsing |
{
"type": "spreadsheet",
"content": "date,description,amount\n2025-05-01,Grocery Store,45.00\n2025-05-02,Subway,12.00\n2025-05-03,Netflix,15.99",
"language": "en"
}
Responseβ
Returns a transaction_list object containing all parsed transactions from the submission. A single submission can produce one or many transactions.
{
"object": "transaction_list",
"data": {
"transactions": [ ... ]
}
}
Errorsβ
| Code | HTTP | When |
|---|---|---|
NOT_FOUND | 404 | The user_id does not exist |
VALIDATION_ERROR | 400 | Required fields missing, invalid type, or unsupported mime_type |
UNAUTHORIZED | 401 | Missing or invalid credentials |
Examples β Text Inputβ
- curl
- Node.js
- Python
curl -X POST \
https://channel.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/transactions \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ=" \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"content": "Spent $45 at the grocery store and $12 on the subway",
"language": "en"
}'
import fetch from 'node-fetch';
const token = Buffer.from('your_client_id:your_client_secret').toString('base64');
const userId = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE';
const response = await fetch(
`https://channel.ozzieapp.com/v1/users/${userId}/transactions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'text',
content: 'Spent $45 at the grocery store and $12 on the subway',
language: 'en',
}),
}
);
const { data } = await response.json();
console.log(`Parsed ${data.transactions.length} transactions:`);
data.transactions.forEach(t => {
console.log(` - ${t.description}: $${(t.amount_cents / 100).toFixed(2)} [${t.category}]`);
});
import requests
import base64
token = base64.b64encode(b'your_client_id:your_client_secret').decode()
user_id = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE'
response = requests.post(
f'https://channel.ozzieapp.com/v1/users/{user_id}/transactions',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
},
json={
'type': 'text',
'content': 'Spent $45 at the grocery store and $12 on the subway',
'language': 'en',
}
)
data = response.json()['data']
print(f"Parsed {len(data['transactions'])} transactions:")
for t in data['transactions']:
print(f" - {t['description']}: ${t['amount_cents'] / 100:.2f} [{t['category']}]")
Example Response β 2 transactions parsed from a text message (201 Created):
{
"object": "transaction_list",
"data": {
"transactions": [
{
"id": "ozz_txn_01HX9Q5NRWBF3KMZP4VC7YDLA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"amount_cents": 4500,
"currency": "USD",
"category": "food",
"description": "Grocery shopping",
"transaction_date": "2025-05-05",
"source": "api_text",
"ai_confidence": 0.97,
"created_at": "2025-05-05T14:34:00Z"
},
{
"id": "ozz_txn_01HX9Q5NRWBF3KMZP4VC7YDLB",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"amount_cents": 1200,
"currency": "USD",
"category": "transport",
"description": "Subway fare",
"transaction_date": "2025-05-05",
"source": "api_text",
"ai_confidence": 0.95,
"created_at": "2025-05-05T14:34:00Z"
}
]
}
}
Ozzie intentionally parses all distinct transactions from a single input. A grocery stop plus a coffee stop in the same message will produce two separate transaction records. This matches real-world usage where users describe their day in a single message.
For receipt images, JPEG and PNG work well. Ensure the image is under 10MB before base64 encoding. Very blurry or rotated images may result in lower ai_confidence scores (below 0.7). Consider prompting the user to retake the photo if confidence is low.
GET /v1/users/{user_id}/transactionsβ
Returns a paginated list of a user's transactions, ordered by transaction_date descending (most recent first).
Path Parametersβ
| Parameter | Description |
|---|---|
user_id | The Ozzie UUID of the user or external:{external_user_id} |
Query Parametersβ
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of transactions per page. Maximum 200. |
cursor | string | β | ISO 8601 datetime cursor for pagination. Returns transactions older than this timestamp. |
from | string | β | Filter: include transactions from this date (YYYY-MM-DD). |
to | string | β | Filter: include transactions up to this date (YYYY-MM-DD). |
category | string | β | Filter by category (e.g., food, transport). Comma-separate for multiple: food,transport. |
Responseβ
{
"object": "list",
"data": {
"transactions": [ ... ],
"has_more": true,
"next_cursor": "2025-04-28T09:15:00Z",
"total_count": 142
}
}
Errorsβ
| Code | HTTP | When |
|---|---|---|
NOT_FOUND | 404 | The user_id does not exist |
VALIDATION_ERROR | 400 | Invalid from/to date format, or limit out of range |
UNAUTHORIZED | 401 | Missing or invalid credentials |
Examplesβ
- curl
- Node.js
- Python
# Transactions for May 2025
curl "https://channel.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/transactions?from=2025-05-01&to=2025-05-31&limit=100" \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ="
# Next page using cursor
curl "https://channel.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/transactions?from=2025-05-01&to=2025-05-31&limit=100&cursor=2025-05-20T10:00:00Z" \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ="
import fetch from 'node-fetch';
const token = Buffer.from('your_client_id:your_client_secret').toString('base64');
const userId = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE';
async function fetchMayTransactions() {
const allTransactions = [];
let cursor = null;
do {
const params = new URLSearchParams({
from: '2025-05-01',
to: '2025-05-31',
limit: '100',
});
if (cursor) params.set('cursor', cursor);
const res = await fetch(
`https://channel.ozzieapp.com/v1/users/${userId}/transactions?${params}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const json = await res.json();
allTransactions.push(...json.data.transactions);
cursor = json.data.has_more ? json.data.next_cursor : null;
} while (cursor);
return allTransactions;
}
const transactions = await fetchMayTransactions();
console.log(`Total May transactions: ${transactions.length}`);
const byCategory = transactions.reduce((acc, t) => {
acc[t.category] = (acc[t.category] || 0) + t.amount_cents;
return acc;
}, {});
Object.entries(byCategory)
.sort(([, a], [, b]) => b - a)
.forEach(([cat, cents]) => {
console.log(` ${cat}: $${(cents / 100).toFixed(2)}`);
});
import requests
import base64
token = base64.b64encode(b'your_client_id:your_client_secret').decode()
user_id = 'ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE'
headers = {'Authorization': f'Bearer {token}'}
all_transactions = []
cursor = None
while True:
params = {'from': '2025-05-01', 'to': '2025-05-31', 'limit': 100}
if cursor:
params['cursor'] = cursor
response = requests.get(
f'https://channel.ozzieapp.com/v1/users/{user_id}/transactions',
headers=headers,
params=params
)
data = response.json()['data']
all_transactions.extend(data['transactions'])
if data['has_more']:
cursor = data['next_cursor']
else:
break
print(f"Total May transactions: {len(all_transactions)}")
by_category = {}
for t in all_transactions:
by_category[t['category']] = by_category.get(t['category'], 0) + t['amount_cents']
for cat, cents in sorted(by_category.items(), key=lambda x: -x[1]):
print(f" {cat}: ${cents / 100:.2f}")
amount_cents is always an integer in the smallest monetary unit (cents for USD/BRL, etc.). Divide by 100 to display dollar values. This avoids floating-point precision issues.
ai_confidenceScores above 0.85 indicate high confidence in the parsed category and amount. Scores between 0.6 and 0.85 may benefit from user confirmation. Scores below 0.6 suggest the input was ambiguous β consider prompting the user to clarify. Ozzie never silently discards low-confidence transactions; they are always returned.