---
title: "How to Build Budgets and Forecast Runway"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_build_budgets_and_forecast_runway-icon.png
date: 2026-01-19T21:58:00.000Z
tag: Product
description: "Most budgeting and runway features break long before the math."
url: "https://unified.to/blog/how_to_build_budgets_and_forecast_runway"
---

# How to Build Budgets and Forecast Runway
------
_January 19, 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](/accounting) 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](/technology) key

## Step 1: Initialize the SDK


```typescript
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` → `AccountingAccount`
- `type` → provider-specific transaction type (Invoice, Bill, Transfer, etc.)
- `created_at` / `updated_at`
- `lineitems[]` (optional)
- `currency`

This sign convention is the foundation of every calculation that follows.


## Step 3: Fetch transactions incrementally


Use `listAccountingTransactions` to sync transactions.


```typescript
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](/docs/reference/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.


```typescript
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:


```typescript
'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

1. **Account type**
    - Include: `EXPENSE`
    - Exclude: `EQUITY`, `LIABILITY`, `FIXED_ASSET`
2. **Sign**
    - Expenses are **positive debits**
    - Refunds/credits appear as negative values
3. **Transaction type**
    - Exclude internal transfers (`Transfer`, `Deposit`)
    - Exclude balance-only movements

Example filter logic:


```typescript
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.


```typescript
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.


```typescript
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:


```typescript
averageMonthlyBurn =
  sum(lastNMonthsSpend) / N;
```


Example (3-month burn):


```typescript
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:


```typescript
runwayMonths =
  currentCash / monthlyBurn;
```


### Getting current cash (two options)


**Option A (preferred):**

- Sum BANK and CREDIT_CARD account balances.

```typescript
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**:


```typescript
const pl = await sdk.accounting.getAccountingProfitloss({
  connectionId,
  id: profitlossId,
  fields: '',
  raw: '',
});
```


Use only:

- `income_total_amount`
- `expenses_total_amount`
- `net_income_amount`
- `expenses_sections[].accounts[].transaction_ids`

Do **not** use deprecated fields.


### Balance Sheet (cash anchor)


```typescript
const bs = await sdk.accounting.getAccountingBalancesheet({
  connectionId,
  id: balancesheetId,
  fields: '',
  raw: '',
});
```


Useful fields:

- `net_assets_amount`
- `assets[]`
- `liabilities[]`

### Cashflow (cross-check)


```typescript
const cf = await sdk.accounting.getAccountingCashflow({
  connectionId,
  id: cashflowId,
  fields: '',
  raw: '',
});
```


Use:

- `cash_beginning_amount`
- `cash_ending_amount`
- `net_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.

→ [Start your 30-day free trial](https://app.unified.to/login)


→ [Book a demo](https://calendly.com/d/cph9-g8n-jzg/connect-with-unified)