# Budgets

Read your company's budgets over the LedgerBee public API (base URL
[`https://api.ledgerbee.com/api/v1`](https://api.ledgerbee.com/api/v1)).
Budgets are created and edited in the LedgerBee app; the API exposes them
read-only. Authenticate with the `x-api-key` header. There is one scope:

- `budgets-read` — every budget read.

The Budgets module is licensed: calls return `403 LICENSE.REQUIRED` unless your
company holds the Budgets license.

A budget plans one accounting year as a grid of amounts per ledger account and
month. Every amount crosses the wire as a decimal string (`"160000.00"`) in
your company's base currency, which each response states as `currency`.

## Operations

| Operation | Endpoint | Scope |
| --- | --- | --- |
| List budgets | `GET /v1/budgets` | `budgets-read` |
| Runway headline | `GET /v1/budgets/runway` | `budgets-read` |
| Get a budget | `GET /v1/budgets/{budgetId}` | `budgets-read` |
| List scenarios | `GET /v1/budgets/{budgetId}/scenarios` | `budgets-read` |
| Scenario grid | `GET /v1/budgets/{budgetId}/scenarios/{scenarioId}/grid` | `budgets-read` |
| Budget vs actuals | `GET /v1/budgets/{budgetId}/comparison` | `budgets-read` |
| Variance vs frozen baseline | `GET /v1/budgets/{budgetId}/variance` | `budgets-read` |
| List frozen baselines | `GET /v1/budgets/{budgetId}/frozen-baselines` | `budgets-read` |
| Get a frozen baseline | `GET /v1/budgets/{budgetId}/frozen-baselines/{baselineId}` | `budgets-read` |
| Re-baseline proposals | `GET /v1/budgets/{budgetId}/rebaseline-proposals` | `budgets-read` |

The list is paginated (`page`, `limit`), filterable by `status`,
`accountingYearId`, and `search` (prefix match on name), and sortable by
`name`, `createdAt`, or `updatedAt`. Endpoint schemas are in the
[API Reference](/api/budgets).

```bash
curl 'https://api.ledgerbee.com/api/v1/budgets?status=ACTIVE' \
  -H 'x-api-key: <your-key>'
```

## The budget grid

`GET /v1/budgets/{budgetId}` returns the full grid: `lines` (one cell per
account and period), the accounting year's `periods`, `driverRows`
(non-monetary planning quantities such as headcount, with numeric values), and
per-budget display metadata (`rowOverrides`, `rowNumbers`).

Cells in periods whose `status` is `CLOSED` carry the booked actuals from the
ledger rather than the planned amount, marked `source: "ACTUALS"` — the same
rolling-forecast view the app shows. A cell filled from the subscription
forecast is marked `source: "SUBSCRIPTION"`; a cell with `source: null` is
user-entered.

Cells can carry a `formula` (for example `=baseline*1.05` or `=#3*2`); the
resolved value is always cached in `amount`, so you never need to evaluate
formulas. `#N` references resolve through the `rowNumbers` array. Pass
`?includeBaseline=true` to add each cell's prior-year realised amount as
`baseline`.

```bash
curl 'https://api.ledgerbee.com/api/v1/budgets/0197a943-2325-7829-b835-b6c71a293066?includeBaseline=true' \
  -H 'x-api-key: <your-key>'
```

## Scenarios

A scenario is an assumption overlay (driver overrides and line toggles) on top
of the budget's base grid. The scenario flagged `isBaseline: true` is the base
grid with no overlay; it is created the first time scenarios are used in the
app, so a budget where they have never been opened returns an empty list — the
base data is the budget detail itself.

`GET /v1/budgets/{budgetId}/scenarios/{scenarioId}/grid` returns the fully
resolved grid with the scenario's assumptions applied. When the response has
`hasCycle: true`, the overlay could not resolve because of a formula cycle;
the offending cells carry `error: "BUDGET_FORMULA_CYCLE"` and amounts must not
be trusted.

## Comparison and variance

Two reports compare the budget against booked actuals, in opposite directions:

| Report | Compares | Variance |
| --- | --- | --- |
| `GET .../comparison` | Live plan vs actuals, per account and period | `budget − actual` |
| `GET .../variance` | Actuals vs a frozen baseline, closed periods only | `actual − frozenBaseline` |

The comparison accepts `viewMode=FISCAL_YEAR` (the budget's accounting year,
the default) or `viewMode=ROLLING_12_MONTHS` with an optional `startMonth`
(`YYYY-MM`). Each entry's `isOverBudget` marks the unfavourable direction for
that account type: overspend on expense accounts, shortfall on revenue
accounts.

The variance report compares against a frozen baseline — an immutable snapshot
of the resolved grid taken in the app. It defaults to the most recently frozen
baseline; pass `?frozenBaselineId=` to pick one from
`GET /v1/budgets/{budgetId}/frozen-baselines`. A budget with no frozen
baseline returns an empty `data` array. Each row's `sentiment` is
`unfavourable` when spending exceeds the frozen plan (expense accounts) or
revenue misses it (revenue accounts).

`GET /v1/budgets/{budgetId}/rebaseline-proposals` lists the re-baseline
suggestions raised when accounting periods close, with their status
(`PROPOSED`, `CONFIRMED`, or `REJECTED`). Proposals are confirmed or rejected
in the app.

## Runway

`GET /v1/budgets/runway` returns the company-wide cash headline: cash on hand
(the sum of bank-account-linked ledger balances), the trailing 3-month burn,
and `monthsRemaining`. Cash and burn are properties of the whole company, so
the endpoint is not scoped under a budget id. Pass `?asOf=YYYY-MM-DD` to
compute as of a past date. When no bank account is linked to a ledger account,
`hasCashSource` is `false` and `status` is `UNKNOWN`.

## Errors

| Condition | Error code | Fix |
| --- | --- | --- |
| Company does not hold the Budgets license | `403 LICENSE.REQUIRED` | Activate the Budgets module in LedgerBee. |
| Key lacks the scope | `403 INSUFFICIENT_PERMISSIONS` | Mint a key with `budgets-read`. |
| Unknown or foreign `budgetId` | `404 BUDGET_NOT_FOUND` | Use an id from `GET /v1/budgets`. |
| Unknown `scenarioId` | `404 BUDGET_SCENARIO_NOT_FOUND` | Use an id from the scenarios list. |
| Unknown `baselineId` | `404 BUDGET_FROZEN_BASELINE_NOT_FOUND` | Use an id from the frozen-baselines list. |
| Invalid filter or query value | A 400 validation error | See the [API Reference](/api/budgets) for accepted values. |
