Fintech App Integration
This guide walks through embedding Ozzie into an existing fintech or banking application. By the end, your app will onboard users into Ozzie automatically, sync their transactions, display their financial plan, and surface an in-app AI coach β all backed by Ozzie's API.
Architecture overviewβ
Your App (Frontend)
|
| API calls
v
Your Backend Server
|
|ββ POST /users (user creation)
|ββ POST /users/{id}/intake (financial intake)
|ββ POST /users/{id}/plan (plan generation)
|ββ POST /users/{id}/goals (goal setting)
|ββ POST /users/{id}/money-moves/generate
|ββ POST /users/{id}/chat/messages
v
Ozzie API (api.ozzieapp.com/v1)
Key principle: your frontend never calls Ozzie directly. All Ozzie API calls are made server-side, where your credentials are secure. Your frontend talks to your backend, which proxies or orchestrates Ozzie calls.
Prerequisitesβ
- An Ozzie API
client_idandclient_secret(get them at the Ozzie dashboard or email commercial@ozzieapp.com) - A backend server (Node.js examples below, but the pattern applies to any stack)
- A mechanism to link your internal user IDs to Ozzie user IDs
Step 1: Create an Ozzie user on signupβ
When a new user completes registration in your app, create their Ozzie profile in the same transaction (or as an immediate async step).
// lib/ozzie.js β shared Ozzie client
const BASE_URL = 'https://api.ozzieapp.com/v1';
const token = Buffer.from(
`${process.env.OZZIE_CLIENT_ID}:${process.env.OZZIE_CLIENT_SECRET}`
).toString('base64');
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
export async function ozzieRequest(method, path, body = null) {
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Ozzie API error: ${error.error?.code} β ${error.error?.message}`);
}
return response.json();
}
// routes/auth.js β called during user registration
import { ozzieRequest } from '../lib/ozzie.js';
import { db } from '../lib/db.js';
export async function handleSignup(req, res) {
const { email, name, language, phone } = req.body;
// 1. Create your internal user
const internalUser = await db.users.create({ email, name, language, phone });
// 2. Create the corresponding Ozzie user
const { data: ozzieUser } = await ozzieRequest('POST', '/users', {
external_user_id: internalUser.id,
name: internalUser.name,
language: internalUser.language || 'en',
phone_e164: internalUser.phone,
});
// 3. Store the Ozzie user ID for future API calls
await db.users.update(internalUser.id, { ozzie_user_id: ozzieUser.id });
res.json({ user: internalUser });
}
Store ozzie_user_id in your users table alongside your own user ID. Every subsequent Ozzie call requires it, so you'll need it on hand for any authenticated request.
Step 2: Collect financial intakeβ
After signup, prompt the user to complete their financial profile. This is the data that powers their plan. You can collect it in a multi-step onboarding flow or a single form.
// routes/onboarding.js
export async function submitFinancialIntake(req, res) {
const { userId } = req.user; // from your auth middleware
const user = await db.users.findById(userId);
const intakePayload = {
monthly_net_income: req.body.monthlyNetIncome, // e.g. 3750.00
monthly_fixed_expenses: req.body.monthlyFixedExpenses, // e.g. 1800.00
monthly_variable_expenses: req.body.monthlyVariableExpenses, // e.g. 950.00
total_debt: req.body.totalDebt, // e.g. 4850.00
monthly_debt_payment: req.body.monthlyDebtPayment, // e.g. 200.00
primary_goal: req.body.primaryGoal, // e.g. "savings"
};
const { data: intake } = await ozzieRequest(
'POST',
`/users/${user.ozzie_user_id}/financial-intake`,
intakePayload
);
res.json({ intake });
}
Step 3: Generate the planβ
Once intake is submitted, generate the user's financial plan. You can do this inline with the intake submission or trigger it as a background job.
// Full onboarding function β intake + plan + goal + first money move
export async function completeOnboarding(userId) {
const user = await db.users.findById(userId);
const ozId = user.ozzie_user_id;
// Submit intake
await ozzieRequest('POST', `/users/${ozId}/financial-intake`, {
monthly_net_income: user.intake.monthly_net_income,
monthly_fixed_expenses: user.intake.monthly_fixed_expenses,
monthly_variable_expenses: user.intake.monthly_variable_expenses,
total_debt: user.intake.total_debt,
monthly_debt_payment: user.intake.monthly_debt_payment,
primary_goal: user.intake.primary_goal,
});
// Generate the plan
const { data: plan } = await ozzieRequest('POST', `/users/${ozId}/plan`);
// Set the user's goal
const { data: goal } = await ozzieRequest('POST', `/users/${ozId}/goals`, {
goal_type: user.intake.primary_goal === 'savings' ? 'savings' : 'debt',
goal_name: user.intake.primary_goal === 'savings' ? 'Emergency Fund' : 'Pay Off Debt',
target_amount: user.intake.primary_goal === 'savings' ? 10000 : 0,
starting_amount: user.intake.primary_goal === 'savings'
? user.intake.current_savings
: user.intake.total_debt,
cadence: 'biweekly',
});
// Generate the first money move cycle
const { data: moneyMove } = await ozzieRequest(
'POST',
`/users/${ozId}/money-moves/generate`
);
return { plan, goal, moneyMove };
}
Step 4: Display Ozzie data in your UIβ
Your frontend fetches data from your backend, which proxies from Ozzie. Here's a pattern for surfacing the plan on a dashboard screen:
// Backend route β proxies plan data to your frontend
app.get('/api/dashboard', requireAuth, async (req, res) => {
const user = await db.users.findById(req.user.id);
const ozId = user.ozzie_user_id;
const [planRes, goalRes, movesRes] = await Promise.all([
ozzieRequest('POST', `/users/${ozId}/plan`),
ozzieRequest('GET', `/users/${ozId}/goals`),
ozzieRequest('GET', `/users/${ozId}/money-moves?status=available&limit=1`),
]);
res.json({
plan: planRes.data,
goal: goalRes.data,
currentMove: movesRes.data[0] ?? null,
});
});
Frontend (React example):
function Dashboard() {
const { data } = useSWR('/api/dashboard', fetcher);
if (!data) return <Spinner />;
const { plan, goal, currentMove } = data;
const progressPct = ((goal.starting_amount - (goal.target_amount || 0)) /
goal.target_amount * 100).toFixed(0);
return (
<div>
<h2>Your Financial Plan</h2>
<BudgetPieChart allocations={plan.allocations} />
<GoalProgressBar
name={goal.goal_name}
progress={progressPct}
targetDate={plan.key_metrics.projected_goal_date}
/>
{currentMove && (
<MoneyMoveCard
tasks={currentMove.tasks}
dueDate={currentMove.due_date}
/>
)}
<ActionItems items={plan.action_items} />
</div>
);
}
Step 5: Embed the AI coachβ
Add a chat interface that forwards user messages to Ozzie's coach:
// Backend route
app.post('/api/chat', requireAuth, async (req, res) => {
const user = await db.users.findById(req.user.id);
const { message } = req.body;
if (!message || message.trim().length === 0) {
return res.status(400).json({ error: 'Message is required' });
}
const { data } = await ozzieRequest(
'POST',
`/users/${user.ozzie_user_id}/chat/messages`,
{ message }
);
res.json(data);
});
// Frontend chat component
function CoachChat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
async function sendMessage() {
const userMessage = input.trim();
if (!userMessage) return;
setMessages(prev => [...prev, { role: 'user', text: userMessage }]);
setInput('');
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage }),
});
const { reply, emotion } = await res.json();
setMessages(prev => [...prev, { role: 'coach', text: reply, emotion }]);
}
return (
<div className="chat-container">
{messages.map((msg, i) => (
<ChatBubble key={i} role={msg.role} text={msg.text} emotion={msg.emotion} />
))}
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={sendMessage}>Send</button>
</div>
);
}
Handling real-time updatesβ
If your app needs to react to changes in a user's financial status (e.g., a new money move becomes available), you can poll or use server-sent events:
// Simple polling strategy β check for new available money moves every 10 minutes
setInterval(async () => {
const users = await db.users.findAll({ where: { is_active: true } });
for (const user of users) {
const { data: moves } = await ozzieRequest(
'GET',
`/users/${user.ozzie_user_id}/money-moves?status=available&limit=1`
);
if (moves.length > 0 && moves[0].id !== user.last_notified_move_id) {
await sendPushNotification(user.id, 'Your new Money Move is ready!');
await db.users.update(user.id, { last_notified_move_id: moves[0].id });
}
}
}, 10 * 60 * 1000);
Tips and best practicesβ
Language detectionβ
Use the user's browser locale or device language to set their Ozzie language preference on creation:
const language = req.headers['accept-language']?.split(',')[0]?.split('-')[0] ?? 'en';
// Supported: 'en', 'pt', 'es'
const ozzieLanguage = ['en', 'pt', 'es'].includes(language) ? language : 'en';
Error handlingβ
Wrap all Ozzie calls in try/catch and handle known error codes gracefully:
try {
const { data } = await ozzieRequest('POST', `/users/${ozId}/plan`);
return data;
} catch (err) {
if (err.message.includes('INTAKE_REQUIRED')) {
// Redirect user to complete their intake
return { redirect: '/onboarding/intake' };
}
if (err.message.includes('RATE_LIMIT_EXCEEDED')) {
// Back off and retry after a delay
await delay(5000);
return ozzieRequest('POST', `/users/${ozId}/plan`);
}
throw err; // Re-throw unknown errors
}
Offline statesβ
Cache Ozzie responses in your database so users see data even when the API is temporarily unavailable:
async function getPlanWithCache(userId) {
const user = await db.users.findById(userId);
// Try Ozzie first
try {
const { data: plan } = await ozzieRequest('POST', `/users/${user.ozzie_user_id}/plan`);
await db.plans.upsert({ user_id: userId, data: plan, cached_at: new Date() });
return plan;
} catch {
// Fall back to cached plan if API is unavailable
const cached = await db.plans.findOne({ user_id: userId });
if (cached) return cached.data;
throw new Error('Plan unavailable and no cache exists');
}
}
Cached plan data can become stale if the user submits new transactions or updates their intake. Invalidate and refresh the cache whenever you submit new data to Ozzie.