How to Build Budgets and Forecast Runway
January 27, 2026
Most budgeting and runway features break long before the math.
They break when products treat accounting reports as inputs rather than outputs. Profit & Loss, Cashflow, and Balance Sheet reports look authoritative, but they are reconstructed views—shaped by provider rules, timing differences, and fields that may change or be deprecated over time.
For PMs and backend teams, this creates real uncertainty:
- Are budgets reacting to real spend, or delayed summaries?
- Is burn calculated from posted activity, or inferred from reports?
- Can runway be trusted when providers compute reports differently?
Teams often discover too late that report-driven logic doesn't generalize across accounting systems. Numbers drift. Assumptions leak into code. Validation becomes difficult.
Unified's Accounting API is designed to avoid this trap. It exposes transaction-level accounting data as the stable foundation for financial analysis—every debit and credit, already classified, timestamped, and incrementally syncable. Reports remain useful, but only as validation layers, not sources of truth.
This guide shows how to build operating budgets and forecast cash runway directly from normalized transaction data—using documented, non-deprecated fields only—so your calculations stay consistent across accounting providers and defensible as your product scales.
Why transactions—not reports—are the source of truth
Accounting reports (Profit & Loss, Cashflow, Balance Sheet) are derived snapshots. They are useful for validation, but they:
- vary by provider,
- may lag behind real activity,
- and include deprecated fields you should not depend on.
Transactions, on the other hand, represent posted financial reality:
- every debit and credit,
- already classified to accounts,
- timestamped,
- and incrementally syncable.
For budgets and runway forecasting:
Transactions are the canonical input. Reports are optional validation.
What this guide covers
- Fetching and incrementally syncing transactions
- Interpreting debit/credit semantics correctly
- Classifying transactions using the chart of accounts
- Building monthly budgets from historical spend
- Calculating burn rate
- Forecasting runway
- Validating results using non-deprecated report totals (optional)
Prerequisites
- Node.js 18+
- A Unified workspace
- An Accounting integration authorized for a customer
- A valid
connectionId - Unified API key
Step 1: Initialize the SDK
import { UnifiedTo } from '@unified-api/typescript-sdk';
const sdk = new UnifiedTo({
security: {
jwt: process.env.UNIFIED_API_KEY!,
},
});
All requests are scoped by connectionId, which represents one authorized customer integration.
Step 2: Understand the Transaction data model (critical)
Unified exposes transactions via AccountingTransaction.
Key fields you must understand:
total_amount- positive = DEBIT
- negative = CREDIT
account_id→AccountingAccounttype→ provider-specific transaction type (Invoice, Bill, Transfer, etc.)created_at/updated_atlineitems[](optional)currency
This sign convention is the foundation of every calculation that follows.
Step 3: Fetch transactions incrementally
Use listAccountingTransactions to sync transactions.
const transactions = await sdk.accounting.listAccountingTransactions({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-22T22:41:23.849Z',
sort: 'updated_at',
order: 'asc',
query: '',
contact_id: '',
fields: '',
raw: '',
});
Pagination rules
- Default
limit: 100 - Maximum
limit: 100 on most endpoints - Pagination uses offset
- Stop when
results.length < limit
Incremental sync
Use updated_gte to fetch only transactions updated since your last checkpoint. This is the recommended polling strategy.
Step 4: Load the chart of accounts
Accounts tell you what kind of transaction you're looking at.
const accounts = await sdk.accounting.listAccountingAccounts({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-22T22:41:23.849Z',
sort: 'updated_at',
order: 'asc',
query: '',
org_id: '',
fields: '',
raw: '',
});
Key account types:
'EXPENSE'
'REVENUE'
'BANK'
'CREDIT_CARD'
'LIABILITY'
'EQUITY'
'FIXED_ASSET'
For runway forecasting, you will primarily use:
- EXPENSE accounts (burn)
- BANK / CREDIT_CARD accounts (cash movement)
- REVENUE (optional, for net burn)
Step 5: Filter to 'true operating spend'
Not every transaction should count toward burn.
Recommended filters
- Account type
- Include:
EXPENSE - Exclude:
EQUITY,LIABILITY,FIXED_ASSET
- Include:
- Sign
- Expenses are positive debits
- Refunds/credits appear as negative values
- Transaction type
- Exclude internal transfers (
Transfer,Deposit) - Exclude balance-only movements
- Exclude internal transfers (
Example filter logic:
const expenseTransactions = transactions.filter(tx => {
const account = accountMap[tx.account_id];
return (
account?.type === 'EXPENSE' &&
tx.total_amount > 0
);
});
Step 6: Bucket spend by month (budget building)
Group transactions into calendar months.
function monthKey(date: string) {
return date.slice(0, 7); // YYYY-MM
}
const monthlySpend = {};
for (const tx of expenseTransactions) {
const month = monthKey(tx.created_at);
monthlySpend[month] = (monthlySpend[month] || 0) + tx.total_amount;
}
This gives you actual historical spend, not estimates.
Step 7: Build category-level budgets (optional)
If categories are present, you can budget by function.
const categories = await sdk.accounting.listAccountingCategories({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-22T22:41:23.849Z',
sort: 'updated_at',
order: 'asc',
query: '',
parent_id: '',
fields: '',
raw: '',
});
Aggregate spend by category_id across transactions to produce budgets like:
- Marketing
- R&D
- G&A
Note: Category availability varies by provider. Your code must tolerate missing categories.
Step 8: Calculate burn rate
Burn rate is typically computed as:
averageMonthlyBurn =
sum(lastNMonthsSpend) / N;
Example (3-month burn):
const recentMonths = Object.values(monthlySpend).slice(-3);
const burnRate =
recentMonths.reduce((a, b) => a + b, 0) / recentMonths.length;
You now have empirical burn, derived directly from transactions.
Step 9: Forecast runway
Runway is:
runwayMonths =
currentCash / monthlyBurn;
Getting current cash (two options)
Option A (preferred):
- Sum BANK and CREDIT_CARD account balances.
const cash = accounts
.filter(a => a.type === 'BANK' || a.type === 'CREDIT_CARD')
.reduce((sum, a) => sum + (a.balance || 0), 0);
Option B (validation):
- Use the Cashflow report (see below).
Step 10: Validate with non-deprecated reports (optional)
Profit & Loss (validation only)
Retrieve a P&L report by ID:
const pl = await sdk.accounting.getAccountingProfitloss({
connectionId,
id: profitlossId,
fields: '',
raw: '',
});
Use only:
income_total_amountexpenses_total_amountnet_income_amountexpenses_sections[].accounts[].transaction_ids
Do not use deprecated fields.
Balance Sheet (cash anchor)
const bs = await sdk.accounting.getAccountingBalancesheet({
connectionId,
id: balancesheetId,
fields: '',
raw: '',
});
Useful fields:
net_assets_amountassets[]liabilities[]
Cashflow (cross-check)
const cf = await sdk.accounting.getAccountingCashflow({
connectionId,
id: cashflowId,
fields: '',
raw: '',
});
Use:
cash_beginning_amountcash_ending_amountnet_change_in_cash_amount
Reports should confirm, not drive, your forecast.
Pagination, rate limits, and operations
- All list endpoints use
limit/offset - Default and max page size is 100
- Rate limits come from:
- the underlying provider
- Unified's fairness limits
- Rate limit errors return HTTP 429
Recommended handling
- Exponential backoff
- Add jitter
- Cap retries
- Prefer webhooks for continuous sync
Summary
Using Unified's Accounting API, you can:
- Build budgets from real posted transactions
- Compute burn rate empirically
- Forecast runway with defensible assumptions
- Validate results using non-deprecated financial reports
- Avoid provider-specific reporting quirks
The key principle is simple:
Transactions first. Reports second. Assumptions last.