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), PDF bank statements, and CSV spreadsheets into structured, categorized transaction records.
A single submission can yield 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",
"description": "Groceries at Whole Foods",
"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) |
description | string | AI-generated human-readable description of the transaction |
transaction_date | string | Date of the transaction 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 | Groceries, restaurants, cafes, food delivery |
transport | Gas, public transit, rideshare, parking, tolls |
housing | Rent, mortgage, home insurance, repairs |
utilities | Electricity, water, internet, phone bill |
health | Pharmacy, doctor visits, gym, health insurance |
entertainment | Streaming services, movies, games, events |
education | Tuition, books, courses, subscriptions for learning |
clothing | Apparel, shoes, accessories |
income | Salary, freelance payment, side income, refund |
other | Anything that doesn't fit the above categories |
source Valuesβ
| Value | Description |
|---|---|
whatsapp_text | Text message received via WhatsApp |
whatsapp_image | Image (receipt/screenshot) received via WhatsApp |
api_text | Text submitted via the REST API |
api_image | Image submitted via the REST API (base64) |
api_pdf | Extracted PDF text submitted via the REST API |
api_spreadsheet | CSV data submitted via the 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 user UUID or external:{external_user_id} |
Request Body β Discriminated Union on typeβ
The request body shape 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". Defaults to the user's language. |
{
"type": "text",
"content": "Spent $45 on groceries at Whole Foods and $12 on the subway",
"language": "en"
}
type: "image"β
Submit a receipt, screenshot, or photo of a purchase as a base64-encoded image. Ozzie performs OCR and parses the result.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "image" |
content | string | Yes | Base64-encoded image data |
mime_type | string | Yes | MIME type of the image: "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 PDF bank statement or receipt. Extract text from the PDF on your end before sending.
| 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,Whole Foods,45.00\n2025-05-02,Subway fare,12.00\n2025-05-03,Netflix,15.99",
"language": "en"
}
Responseβ
Returns a transaction_list object containing all transactions parsed from the submission. A single submission may 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 | Missing required fields, invalid type, or unsupported mime_type |
UNAUTHORIZED | 401 | Missing or invalid credentials |
Examples β Text Inputβ
- curl
- Node.js
- Python
curl -X POST \
https://api.ozzieapp.com/v1/users/ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE/transactions \
-H "Authorization: Bearer czJjbGllbnQ6czJzZWNyZXQ=" \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"content": "Spent $45 on groceries at Whole Foods 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://api.ozzieapp.com/v1/users/${userId}/transactions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'text',
content: 'Spent $45 on groceries at Whole Foods 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://api.ozzieapp.com/v1/users/{user_id}/transactions',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
},
json={
'type': 'text',
'content': 'Spent $45 on groceries at Whole Foods 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 one 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": "Groceries at Whole Foods",
"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 run 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 one message.
For receipt images, JPEG and PNG both work well. Make sure 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 transactions for a user, ordered by transaction_date descending (most recent first).
Path Parametersβ
| Parameter | Description |
|---|---|
user_id | The Ozzie user UUID 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 on or after this date (YYYY-MM-DD). |
to | string | β | Filter: include transactions on or before 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://api.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://api.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';
// Fetch all transactions for May 2025 (paginated)
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://api.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}`);
// Summarize by category
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}'}
# Fetch all May 2025 transactions (paginated)
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://api.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)}")
# Summarize by category
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}")
Example Response (200 OK) β with date range filter:
{
"object": "list",
"data": {
"transactions": [
{
"id": "ozz_txn_01HX9Q5NRWBF3KMZP4VC7YDLA",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"amount_cents": 4500,
"currency": "USD",
"category": "food",
"description": "Groceries at Whole Foods",
"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"
},
{
"id": "ozz_txn_01HX9R2KVQPF4LNYB5WD8ZCMG",
"user_id": "ozz_usr_01HX9KZMR4P5JQNBVT7YCW3DE",
"amount_cents": 1599,
"currency": "USD",
"category": "entertainment",
"description": "Netflix subscription",
"transaction_date": "2025-05-03",
"source": "api_pdf",
"ai_confidence": 0.99,
"created_at": "2025-05-04T09:20:00Z"
}
],
"has_more": true,
"next_cursor": "2025-05-03T00:00:00Z",
"total_count": 47
}
}
amount_cents is always an integer in the smallest currency unit (cents for USD, pence for GBP, etc.). Divide by 100 to display dollar amounts. This avoids floating-point precision issues.
ai_confidence interpretationScores 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.