Skip to main content

Analytics Dashboard

This guide covers pulling Ozzie data into a BI or analytics dashboard. You can aggregate transaction data, track plan adherence, measure goal completion rates, and build reporting views β€” either for your own internal use or as a customer-facing product feature.


What data is available​

Data typeEndpointKey fields
TransactionsGET /users/{id}/transactionsamount, category, merchant, date, source
Plan allocationsPOST /users/{id}/planneeds/wants/savings/debt % breakdown
Goal progressGET /users/{id}/goalstarget_amount, starting_amount, next_move_date
Money move completionGET /users/{id}/money-movesstatus per cycle, task completion rates
Financial intakeGET /users/{id}/financial-intakeincome, expenses, debt snapshot

Key metrics you can compute​

MetricHow to compute
Monthly spending by categorySum amount_cents grouped by category for transactions in the current month
Savings rate(monthly_net_income - total_expenses) / monthly_net_income * 100 from intake
Goal progress %(current_amount - starting_amount) / (target_amount - starting_amount) * 100
Money move completion ratecompleted cycles / total cycles * 100 per user or across all users
Average savings per cycleSum of amount_cents for do tasks marked done, divided by cycle count
Plan adherenceCompare actual spending categories from transactions vs. plan allocations targets

Pagination strategy for bulk export​

Ozzie uses cursor-based pagination on all list endpoints. For a full data export, iterate until has_more is false:

// lib/ozzie.js
import { ozzieRequest } from './ozzie-client.js';

/**
* Fetches all pages of a list endpoint and returns the full dataset.
* @param {string} path - e.g. `/users/usr_abc/transactions`
* @param {Object} params - Query parameters (excluding cursor)
*/
export async function fetchAllPages(path, params = {}) {
const allItems = [];
let cursor = null;
let hasMore = true;

while (hasMore) {
const queryParams = new URLSearchParams({ limit: '50', ...params });
if (cursor) queryParams.set('cursor', cursor);

const { data, pagination } = await ozzieRequest('GET', `${path}?${queryParams}`);

allItems.push(...data);
hasMore = pagination.has_more;
cursor = pagination.next_cursor;
}

return allItems;
}

Data export function​

This function exports all financial data for a user into a structured object ready for a BI tool or data warehouse:

// lib/export.js

import { fetchAllPages, ozzieRequest } from './ozzie.js';

export async function exportUserFinancialData(ozzieUserId) {
const [transactions, moneyMoves, goalsRes, planRes, intakeRes] = await Promise.all([
fetchAllPages(`/users/${ozzieUserId}/transactions`),
fetchAllPages(`/users/${ozzieUserId}/money-moves`),
ozzieRequest('GET', `/users/${ozzieUserId}/goals`).catch(() => ({ data: null })),
ozzieRequest('POST', `/users/${ozzieUserId}/plan`).catch(() => ({ data: null })),
ozzieRequest('GET', `/users/${ozzieUserId}/financial-intake`).catch(() => ({ data: null })),
]);

return {
user_id: ozzieUserId,
exported_at: new Date().toISOString(),
transactions,
money_moves: moneyMoves,
goal: goalsRes.data,
plan: planRes.data,
intake: intakeRes.data,
};
}

Sample queries and API calls​

Monthly spending by category​

Fetch all transactions for a month and aggregate by category:

async function getMonthlySpendingByCategory(ozzieUserId, year, month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of month

const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
start_date: startDate,
end_date: endDate,
});

// Group and sum by category
const byCategory = transactions.reduce((acc, tx) => {
const category = tx.category ?? 'Uncategorized';
acc[category] = (acc[category] ?? 0) + tx.amount_cents;
return acc;
}, {});

// Convert cents to dollars and sort descending
return Object.entries(byCategory)
.map(([category, cents]) => ({ category, amount: cents / 100 }))
.sort((a, b) => b.amount - a.amount);
}

// Example output:
// [
// { category: "Housing", amount: 1200.00 },
// { category: "Food & Dining", amount: 487.50 },
// { category: "Transportation", amount: 210.00 },
// { category: "Entertainment", amount: 134.00 },
// { category: "Utilities", amount: 120.00 },
// ]

Savings rate over time​

Compute the savings rate for each of the past N months across your user base:

async function getSavingsRateTrend(ozzieUserIds, monthsBack = 6) {
const results = [];

for (let i = 0; i < monthsBack; i++) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const label = `${year}-${String(month).padStart(2, '0')}`;

let totalSaved = 0;
let totalIncome = 0;

for (const userId of ozzieUserIds) {
const { data: intake } = await ozzieRequest('GET', `/users/${userId}/financial-intake`).catch(() => ({ data: null }));
if (!intake) continue;

const monthTransactions = await fetchAllPages(`/users/${userId}/transactions`, {
start_date: `${year}-${String(month).padStart(2, '0')}-01`,
});

const spent = monthTransactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const income = intake.monthly_net_income;
const saved = Math.max(0, income - spent);

totalIncome += income;
totalSaved += saved;
}

results.push({
month: label,
savings_rate: totalIncome > 0 ? ((totalSaved / totalIncome) * 100).toFixed(1) : '0.0',
total_saved: totalSaved.toFixed(2),
});
}

return results.reverse(); // Oldest first
}

// Example output:
// [
// { month: "2024-12", savings_rate: "9.2", total_saved: "3450.00" },
// { month: "2025-01", savings_rate: "11.4", total_saved: "4275.00" },
// { month: "2025-02", savings_rate: "10.8", total_saved: "4050.00" },
// { month: "2025-03", savings_rate: "13.1", total_saved: "4912.50" },
// { month: "2025-04", savings_rate: "14.7", total_saved: "5512.50" },
// { month: "2025-05", savings_rate: "15.0", total_saved: "5625.00" },
// ]

Goal completion percentage​

Compute progress toward a savings or debt goal:

async function getGoalProgress(ozzieUserId) {
const { data: goal } = await ozzieRequest('GET', `/users/${ozzieUserId}/goals`);
if (!goal) return null;

const { goal_type, goal_name, target_amount, starting_amount } = goal;

if (goal_type === 'savings') {
// For savings: progress is current balance vs target
// (In a real app, current balance = starting_amount + total deposits since goal creation)
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
category: 'Savings Transfer',
});

const deposited = transactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const currentAmount = starting_amount + deposited;
const progressPct = Math.min(100, (currentAmount / target_amount) * 100);

return {
goal_name,
goal_type,
starting_amount,
current_amount: currentAmount,
target_amount,
progress_pct: progressPct.toFixed(1),
remaining: Math.max(0, target_amount - currentAmount),
};
}

if (goal_type === 'debt') {
// For debt: progress is how much has been paid down from starting_amount
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
category: 'Debt Payment',
});

const paid = transactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const remaining = Math.max(0, starting_amount - paid);
const progressPct = Math.min(100, (paid / starting_amount) * 100);

return {
goal_name,
goal_type,
starting_amount,
paid_down: paid,
remaining,
target_amount: 0,
progress_pct: progressPct.toFixed(1),
};
}
}

// Example output (savings goal):
// {
// goal_name: "Emergency Fund",
// goal_type: "savings",
// starting_amount: 1200.00,
// current_amount: 2850.00,
// target_amount: 10000.00,
// progress_pct: "28.5",
// remaining: 7150.00
// }

Money move completion rate​

async function getMoneyMoveCompletionRate(ozzieUserId) {
const allMoves = await fetchAllPages(`/users/${ozzieUserId}/money-moves`);

const terminal = allMoves.filter(m => ['completed', 'skipped'].includes(m.status));
const completed = allMoves.filter(m => m.status === 'completed');

if (terminal.length === 0) return { completion_rate: null, cycles: 0 };

return {
total_cycles: terminal.length,
completed_cycles: completed.length,
skipped_cycles: terminal.length - completed.length,
completion_rate: ((completed.length / terminal.length) * 100).toFixed(1),
};
}

Bulk export across all users​

// scripts/export-all.js β€” run as a nightly batch job

import { db } from '../lib/db.js';
import { exportUserFinancialData } from '../lib/export.js';
import { writeFileSync } from 'fs';

async function exportAll() {
const users = await db.users.findAll({ where: { ozzie_user_id: { not: null } } });

console.log(`Exporting data for ${users.length} users...`);

const results = [];
const errors = [];

for (const user of users) {
try {
const data = await exportUserFinancialData(user.ozzie_user_id);
results.push({ internal_user_id: user.id, ...data });
} catch (err) {
errors.push({ user_id: user.id, error: err.message });
}

// Rate-limit-friendly delay between users
await new Promise(r => setTimeout(r, 100));
}

const output = {
exported_at: new Date().toISOString(),
user_count: results.length,
error_count: errors.length,
users: results,
errors,
};

writeFileSync(`exports/ozzie-${Date.now()}.json`, JSON.stringify(output, null, 2));
console.log(`Export complete. ${results.length} users, ${errors.length} errors.`);
}

exportAll();
info

For very large user bases (10k+ users), run exports in parallel batches of 10–20 users, staying within your rate limit. Check X-RateLimit-Remaining response headers to avoid hitting the limit.


Suggested charts​

Spending breakdown β€” pie or donut chart​

Use getMonthlySpendingByCategory() data:

Category | Amount | % of Total
--------------------|---------|------------
Housing | $1,200 | 45.6%
Food & Dining | $487 | 18.5%
Transportation | $210 | 8.0%
Entertainment | $134 | 5.1%
Utilities | $120 | 4.6%
Other | $480 | 18.2%

Chart type: Pie or donut. Color-code by category. Add a center label showing total monthly spend.

Overlay: Draw a dotted ring showing the plan's allocations.wants and allocations.needs targets to show budget vs. actual.


Savings rate trend β€” line chart​

Use getSavingsRateTrend() data over 6–12 months:

Month | Savings Rate
---------|-------------
Dec 2024 | 9.2%
Jan 2025 | 11.4%
Feb 2025 | 10.8%
Mar 2025 | 13.1%
Apr 2025 | 14.7%
May 2025 | 15.0%

Chart type: Line chart with monthly x-axis. Add a horizontal dashed line at the plan's target_savings_rate so users can see when they crossed the threshold.


Goal progress β€” horizontal progress bar​

Use getGoalProgress() data:

Emergency Fund
[$1,200 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ $10,000]
28.5% β€” $7,150 to go β€” Est. Nov 2029

Chart type: Horizontal progress bar with label showing percentage, amount remaining, and projected date from plan.key_metrics.projected_goal_date.


Money move completion β€” bar or gauge chart​

Use getMoneyMoveCompletionRate() data:

Completion Rate: 78% (14 completed / 18 total cycles)

Chart type: Gauge or simple stat card. For a user-facing dashboard, show a streak counter (consecutive completed cycles) to encourage consistency.