Budgets
Read your company's budgets over the LedgerBee public API (base URL
https://api.ledgerbee.com/api/v1), and
overwrite a budget's figures from an edited copy of its Excel workbook.
Budgets are created and structured in the LedgerBee app. Authenticate with the
x-api-key header. There are two scopes:
budgets-read— every budget read, and the workbook export.budgets-write— uploading a workbook, as a dry run or a publish.
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 |
| Export the Excel workbook | GET /v1/budgets/{budgetId}/excel | budgets-read |
| Upload a workbook (dry run or publish) | POST /v1/budgets/{budgetId}/excel | budgets-write |
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.
Code
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.
Code
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.
A driver row's cells are the budget's own driver cells with the scenario's
values applied. When the scenario sets a month the budget left empty, no driver
cell exists for it, and the month is listed on the row in scenarioOnlyMonths
instead, with scenarioOverrideId, accountingPeriodId and value.
scenarioOnlyMonths appears on scenario grids only, and its
scenarioOverrideId identifies the scenario change, not a driver cell.
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.
Updating figures from Excel
You change a budget's figures the way the app's Excel round trip does: download the budget's workbook, change figures in it, and upload it back. The workbook has one row per account (and dimension value) and one column per accounting period.
GET /v1/budgets/{budgetId}/excelreturns the workbook (.xlsx).- Change figures in it, in Excel or in code. Keep its hidden sheet: it identifies the budget, its accounting year and the figures the file was exported with.
POST /v1/budgets/{budgetId}/excel?dryRun=true, with the file as the multipart fieldfile, checks the file and answers every figure it would change. Nothing is written.- The same request without
dryRunpublishes the file.
Code
A dry run and a publish answer the same response. changes lists every figure
that changes, with previousAmount and newAmount as decimal strings in
currency. A change with origin: "FILE" was typed into the workbook; one
with origin: "RECALCULATED" is a formula figure that moves because a figure
it reads changed. refusedRows lists the rows that cannot be read against the
budget, such as an unknown account number or an amount that is not a number.
canPublish is true when no row is refused and at least one figure changes.
After a publish, publication describes the upload, which the app keeps in the
budget's upload history with its file.
Only the figures typed into the workbook are written. A figure the workbook
changes loses its formula or live source; every other figure keeps its own. A
figure that changed in LedgerBee after the workbook was exported carries
changedSinceExport: true when the workbook overwrites it.
Holding a publish to its dry run
The dry run answers a previewToken. Pass it to the publish as
?previewToken= and the publish is refused with 409 BUDGET_EXCEL_PREVIEW_STALE
if the budget changed between the two, so a publish writes only what its dry
run showed. Run the dry run again and publish with the new token.
Retries
Send an x-api-idempotency-key header and a retry with the same key is
answered with the original response instead of running again. Use a new key
for each request: a dry run and the publish after it are two requests.
From an AI assistant
Over MCP an assistant reads and sets figures as data, with no workbook in
between. The budgets toolset holds two tools, and is not among the toolsets a
new connection enables, so tick it when you connect. Reading needs the
budgets-read scope and setting figures needs budgets-write.
get_budgetreturns the budget's periods and one row per account (and dimension value) with a figure per period, in the company's base currency.accountNumbersnarrows it to the accounts you name.set_budget_figurestakes a list of changes. Each names anaccountNumber, aperiod(the period's id, its start date such as2026-12-01, or the month it starts in such as2026-12), abaseCurrencyAmountand, on a budget split by a dimension, adimensionValueId. A requireddryRunand an optionalpreviewTokenwork as on the REST upload, and the changes go through the same checks and the same publish.
The answer refers to the changes by their position in the request:
changes[].changeIndex for each figure a change moves (null for a formula
figure that recalculates), refusedChanges for the changes that cannot be
applied, and alreadyHeld for the changes whose figure the budget already
holds. A publish is recorded in the upload history under the user who connected
the assistant, and keeps the budget's workbook with the figures typed in.
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, or budgets-write to upload a workbook. |
Unknown or foreign budgetId | 404 BUDGET_NOT_FOUND | Use an id from GET /v1/budgets. |
The upload carries no file part | 400 IMPORT_NO_FILES_PROVIDED | Send the workbook as the multipart field file. |
The file is not a .xlsx, .xls or .csv | 400 IMPORT_FILE_TYPE_NOT_ALLOWED | Upload the .xlsx the export returned. |
| The file is over 5 MB | 413 | Remove rows and columns that are not part of the export. |
| The workbook holds more than 20,000 rows or 100 columns | 400 BUDGET_EXCEL_FILE_TOO_LARGE | Remove rows and columns that are not part of the export. |
| The file is not an exported budget workbook | 400 BUDGET_EXCEL_NOT_A_BUDGET_EXPORT | Start from GET /v1/budgets/{budgetId}/excel and keep its hidden sheet. |
| The file was exported from another budget, year or currency | 400 BUDGET_EXCEL_WRONG_BUDGET, BUDGET_EXCEL_WRONG_YEAR, BUDGET_EXCEL_WRONG_CURRENCY | Export this budget's workbook and edit that. |
| The budget's dimension changed after the export | 400 BUDGET_EXCEL_DIMENSION_MISMATCH | Export the workbook again. |
| A publish with refused rows | 400 BUDGET_EXCEL_HAS_REFUSED_ROWS | Read refusedRows from a dry run and fix those rows. |
| A publish that changes no figure | 400 BUDGET_EXCEL_NOTHING_TO_PUBLISH | The budget already holds every figure in the file. |
| The budget changed after the dry run | 409 BUDGET_EXCEL_PREVIEW_STALE | Run the dry run again and publish with its new previewToken. |
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 for accepted values. |