How to Perform Financial Analysis with Transaction-Level Detail Using Unified's Accounting API
January 10, 2026
Most budgeting and runway features fail long before the math.
On paper, financial analysis looks simple. Aggregate transactions. Group by account. Compare actuals to budget. Divide cash by burn. In practice, those steps only hold if the underlying ledger data is consistent across customers.
That's where product complexity creeps in.
Accounting platforms don't just differ in UI. They model transactions, accounts, balances, and even fiscal structure differently. Some systems expose clean transaction-level debits and credits. Others infer direction from context. Account hierarchies vary. Balance sheet snapshots may or may not align cleanly with transaction timelines.
For a PM, this creates uncomfortable tradeoffs:
- Can we guarantee that budgets mean the same thing across accounting providers?
- Can runway be calculated consistently, or does it depend on which system the customer uses?
- Can we roll this feature out broadly, or only to a subset of 'well-behaved' integrations?
Many products solve this by narrowing scope or hardcoding assumptions per vendor. That works early, but it limits who the feature works for and how confidently you can ship changes.
Unified's Accounting API is designed to remove those constraints. Instead of reconciling accounting semantics downstream, Unified normalizes transactions, accounts, organizations, and balance sheets upstream. Signed amounts, account classifications, and timestamps behave consistently before your analysis logic ever runs.
This guide shows how to build transaction-level financial analysis—monthly actuals, baseline budgets, and runway—on top of that normalized layer, without branching logic for QuickBooks vs. Xero vs. NetSuite, and without rewriting your analysis as new accounting systems are added.
Prerequisites
- Node.js v18+
- A Unified account with an Accounting integration enabled
- Your Unified API key
- A customer Accounting
connectionId
Step 1: Set up your project
mkdir transaction-finance-analysis-demo
cd transaction-finance-analysis-demo
npm init -y
npm install @unified-api/typescript-sdk dotenv
Create a .env file:
UNIFIED_API_KEY=your_unified_api_key
CONNECTION_ACCOUNTING=your_customer_accounting_connection_id
Step 2: Initialize the SDK
import "dotenv/config";
import { UnifiedTo } from "@unified-api/typescript-sdk";
const { UNIFIED_API_KEY, CONNECTION_ACCOUNTING } = process.env;
const sdk = new UnifiedTo({
security: { jwt: UNIFIED_API_KEY! },
});
Step 3: Understand the normalized Accounting objects (critical)
Unified's Accounting models use snake_case field names in the API docs and TypeScript types shown here.
Transactions (AccountingTransaction)
Transactions are the lowest-level record of ledger activity. Useful fields include:
total_amount(signed: negative for credit, positive for debit)account_id(where the transaction lands in the chart of accounts)currencytype(e.g. Invoice, BillPaymentCreditCard, JournalEntry, etc.)contacts[](counterparty hints:is_supplier,is_customer)created_at,updated_at(timestamps for bucketing and incremental export)
Accounts (AccountingAccount)
Accounts label account_id and provide the classification you need for budgets and burn:
id,nametype(e.g.EXPENSE,REVENUE,BANK,CREDIT_CARD, etc.)- hierarchy via
parent_id
Organization (AccountingOrganization)
Organization gives you company metadata helpful for analysis:
currency(primary currency)fiscal_year_end_month
Balance sheet (AccountingBalancesheet)
Balance sheet provides point-in-time balances:
currency,net_assets_amountassets[],liabilities[],equity[]withaccount_id,amount
Step 4: Fetch organizations (to get primary currency and fiscal year end)
Below are partial TypeScript shapes showing only the fields used in this example.
import type { UnifiedTo } from "@unified-api/typescript-sdk";
export type AccountingOrganization = {
id?: string;
name: string;
currency?: string;
fiscal_year_end_month?: number;
};
export async function fetchAllOrganizations(
sdk: UnifiedTo,
connectionId: string,
opts?: {
pageSize?: number;
updated_gte?: string;
sort?: "name" | "updated_at" | "created_at";
order?: "asc" | "desc";
query?: string;
fields?: string;
raw?: string;
}
): Promise<AccountingOrganization[]> {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const out: AccountingOrganization[] = [];
while (true) {
const page = await sdk.accounting.listAccountingOrganizations({
connectionId,
limit: pageSize,
offset,
updated_gte: opts?.updated_gte,
sort: opts?.sort ?? "updated_at",
order: opts?.order ?? "asc",
query: opts?.query ?? "",
fields: opts?.fields ?? "",
raw: opts?.raw ?? "",
});
if (!page || page.length === 0) break;
out.push(...page);
offset += pageSize;
}
return out;
}
export function pickPrimaryOrganization(orgs: AccountingOrganization[]): AccountingOrganization | null {
if (!orgs || orgs.length === 0) return null;
// Use the first org by default. If you have a known org selection strategy, apply it here.
return orgs[0];
}
Step 5: Fetch the chart of accounts (for classification)
import type { UnifiedTo } from "@unified-api/typescript-sdk";
export type AccountingAccount = {
id?: string;
name?: string;
type?:
| "ACCOUNTS_PAYABLE"
| "ACCOUNTS_RECEIVABLE"
| "BANK"
| "CREDIT_CARD"
| "FIXED_ASSET"
| "LIABILITY"
| "EQUITY"
| "EXPENSE"
| "REVENUE"
| "OTHER";
parent_id?: string;
currency?: string;
balance?: number;
};
export async function fetchAllAccounts(
sdk: UnifiedTo,
connectionId: string,
opts?: {
pageSize?: number;
updated_gte?: string;
sort?: "name" | "updated_at" | "created_at";
order?: "asc" | "desc";
query?: string;
org_id?: string;
fields?: string;
raw?: string;
}
): Promise<AccountingAccount[]> {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const out: AccountingAccount[] = [];
while (true) {
const page = await sdk.accounting.listAccountingAccounts({
connectionId,
limit: pageSize,
offset,
updated_gte: opts?.updated_gte,
sort: opts?.sort ?? "updated_at",
order: opts?.order ?? "asc",
query: opts?.query ?? "",
org_id: opts?.org_id ?? "",
fields: opts?.fields ?? "",
raw: opts?.raw ?? "",
});
if (!page || page.length === 0) break;
out.push(...page);
offset += pageSize;
}
return out;
}
export function indexAccounts(accounts: AccountingAccount[]): Record<string, AccountingAccount> {
return Object.fromEntries(accounts.filter((a) => a.id).map((a) => [a.id!, a]));
}
Step 6: Fetch all transactions (transaction-level detail)
import type { UnifiedTo } from "@unified-api/typescript-sdk";
export type AccountingTransaction = {
id?: string;
created_at?: string;
updated_at?: string;
memo?: string;
total_amount?: number; // negative for CREDIT, positive for DEBIT
currency?: string;
account_id?: string;
type?: string;
contacts?: {
id?: string;
is_customer?: boolean;
is_supplier?: boolean;
}[];
};
export async function fetchAllTransactions(
sdk: UnifiedTo,
connectionId: string,
opts?: {
pageSize?: number;
updated_gte?: string;
sort?: "name" | "updated_at" | "created_at";
order?: "asc" | "desc";
query?: string;
contact_id?: string;
fields?: string;
raw?: string;
}
): Promise<AccountingTransaction[]> {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const out: AccountingTransaction[] = [];
while (true) {
const page = await sdk.accounting.listAccountingTransactions({
connectionId,
limit: pageSize,
offset,
updated_gte: opts?.updated_gte,
sort: opts?.sort ?? "updated_at",
order: opts?.order ?? "asc",
query: opts?.query ?? "",
contact_id: opts?.contact_id ?? "",
fields: opts?.fields ?? "",
raw: opts?.raw ?? "",
});
if (!page || page.length === 0) break;
out.push(...page);
offset += pageSize;
}
return out;
}
Step 7: Build monthly actuals (by account)
This step turns transaction detail into a month-bucketed view of spend. For budgeting and runway, the most useful 'actuals' are typically expense accounts (AccountingAccount.type === "EXPENSE").
export type MonthlyActuals = Record<string, Record<string, number>>;
// monthKey -> accountId -> total_amount
function toMonthKey(iso: string | undefined): string | null {
if (!iso) return null;
const s = String(iso);
if (s.length < 7) return null;
return s.slice(0, 7); // YYYY-MM
}
export function buildMonthlyExpenseActuals(
transactions: AccountingTransaction[],
accountIndex: Record<string, { type?: string }>
): MonthlyActuals {
const out: MonthlyActuals = {};
for (const t of transactions) {
const month = toMonthKey(t.created_at);
if (!month) continue;
const accountId = t.account_id;
if (!accountId) continue;
const acct = accountIndex[accountId];
if (!acct || acct.type !== "EXPENSE") continue;
const amt = Number(t.total_amount ?? 0);
out[month] ||= {};
out[month][accountId] = (out[month][accountId] ?? 0) + amt;
}
return out;
}
Step 8: Estimate a baseline budget (from trailing averages)
A simple baseline budget can be computed as the average monthly spend per expense account over the last N months.
export function suggestMonthlyBudgets(
monthlyActuals: MonthlyActuals,
monthsBack = 3
): Record<string, number> {
const months = Object.keys(monthlyActuals).sort(); // YYYY-MM sort works lexicographically
const recent = months.slice(-monthsBack);
const sums: Record<string, number> = {};
for (const m of recent) {
for (const [accountId, amt] of Object.entries(monthlyActuals[m] || {})) {
sums[accountId] = (sums[accountId] ?? 0) + Number(amt ?? 0);
}
}
const budgets: Record<string, number> = {};
for (const [accountId, total] of Object.entries(sums)) {
budgets[accountId] = total / Math.max(1, recent.length);
}
return budgets;
}
Step 9: Fetch the latest balance sheet and calculate runway
Runway requires a cash-on-hand estimate. This implementation uses balance sheet assets[] and filters to accounts whose AccountingAccount.type is BANK.
import type { UnifiedTo } from "@unified-api/typescript-sdk";
export type AccountingBalancesheet = {
id?: string;
created_at?: string;
updated_at?: string;
start_at?: string;
end_at?: string;
currency?: string;
net_assets_amount?: number;
assets?: { account_id?: string; amount?: number; name?: string }[];
};
export async function fetchLatestBalancesheet(
sdk: UnifiedTo,
connectionId: string
): Promise<AccountingBalancesheet | null> {
const page = await sdk.accounting.listAccountingBalancesheets({
connectionId,
limit: 1,
offset: 0,
sort: "updated_at",
order: "desc",
query: "",
start_gte: "",
end_lt: "",
end_le: "",
category_id: "",
contact_id: "",
fields: "",
raw: "",
updated_gte: "",
});
if (!page || page.length === 0) return null;
return page[0];
}
export function estimateCashOnHand(
balancesheet: AccountingBalancesheet,
accountIndex: Record<string, AccountingAccount>
): number {
const assets = balancesheet.assets ?? [];
let cash = 0;
for (const a of assets) {
if (!a.account_id) continue;
const acct = accountIndex[a.account_id];
if (!acct) continue;
if (acct.type === "BANK") {
cash += Number(a.amount ?? 0);
}
}
return cash;
}
export function estimateRunwayMonths(
cashOnHand: number,
monthlyExpenseBurn: number
): number | null {
const burn = Number(monthlyExpenseBurn ?? 0);
if (burn <= 0) return null;
return cashOnHand / burn;
}
Step 10: Putting it all together
This example pulls accounts + transactions, builds monthly actuals, suggests a baseline budget, fetches the latest balance sheet, and estimates runway.
async function main() {
const connectionId = CONNECTION_ACCOUNTING!;
if (!connectionId) throw new Error("Missing CONNECTION_ACCOUNTING");
const [orgs, accounts, transactions] = await Promise.all([
fetchAllOrganizations(sdk, connectionId, { pageSize: 50 }),
fetchAllAccounts(sdk, connectionId, { pageSize: 100 }),
fetchAllTransactions(sdk, connectionId, { pageSize: 100 }),
]);
const org = pickPrimaryOrganization(orgs);
console.log("Organization:", org?.name, org?.currency, org?.fiscal_year_end_month);
const accountIndex = indexAccounts(accounts);
const monthlyActuals = buildMonthlyExpenseActuals(transactions, accountIndex);
console.log("Monthly expense actuals (months):", Object.keys(monthlyActuals));
const budgets = suggestMonthlyBudgets(monthlyActuals, 3);
console.log("Suggested monthly budgets (per account_id):", budgets);
const latestBs = await fetchLatestBalancesheet(sdk, connectionId);
if (!latestBs) {
console.log("No balancesheet available.");
return;
}
const cashOnHand = estimateCashOnHand(latestBs, accountIndex);
// Average monthly burn: sum of suggested budgets
const avgMonthlyBurn = Object.values(budgets).reduce((sum, v) => sum + Number(v ?? 0), 0);
const runway = estimateRunwayMonths(cashOnHand, avgMonthlyBurn);
console.log("Cash on hand:", cashOnHand);
console.log("Avg monthly burn:", avgMonthlyBurn);
console.log("Estimated runway (months):", runway);
}
main().catch(console.error);
You now have a transaction-level financial analysis flow that:
- pulls normalized ledger transactions with pagination
- labels transactions using the chart of accounts
- constructs month-bucketed actuals for budgets
- estimates baseline budgets from trailing averages
- fetches the latest balance sheet and computes runway from cash-on-hand and burn
From here, you can extend the same dataset to support per-vendor spend views (via transaction contacts[]), budgeting by category trees, or cashflow-based runway calculations when your accounting integration supports those statements.