API reference
Budget Tool exposes a read API so another system can pull budgets, actuals and variance without anybody exporting a spreadsheet. It is the same data behind the same rules: a token narrows what its owner can already do, and never widens it.
Version 1.0.0. Every path below is relative to /api/v1.
Start here
Three ways in, depending on what you are doing:
The collection imports into Postman, Insomnia and Bruno. Both files are generated from the same contract as this page, so none of the three can disagree about what the API does.
Using the collection
- Import the file. You will get one folder per group of endpoints.
- Open the collection's Variables tab.
- Set
apiTokento a token from Admin → API tokens. CheckbaseUrlpoints at the right installation. - Send any request. Query filters are included but switched off, so the first call returns real data rather than an empty page.
The token field ships empty on purpose. A collection with a working credential in it is a credential in every copy anybody makes of that file.
Authenticating
An administrator creates a token under Admin → API tokens. It is shown once, at creation, and stored only as a hash — if it is lost it is replaced rather than recovered.
curl -s 'https://mushreeves.com/api/v1/budgets' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
A browser session cannot authenticate an API call, deliberately: the API firewall is stateless and token-only, so a cookie that happens to be lying around cannot speak for you.
Endpoints
Budgets
GET
/budget-years
scope: budgets:read
List budget years
Newest year first. Requires the `budgets:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/budget-years' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 22,
"year": 2027,
"name": "Budget 2027",
"status": "draft"
},
{
"id": 21,
"year": 2026,
"name": "Budget 2026",
"status": "approved"
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 3
}
}
GET
/budgets
scope: budgets:read
List budgets
Requires the `budgets:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
companyId
|
query | Return only budgets belonging to this company |
budgetYearId
|
query | Return only budgets in this budget year |
Request
curl -s 'https://budget.mushreeves.com/api/v1/budgets' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 162,
"name": "Manchester Clinic 2027",
"status": "draft",
"budgetYear": {
"id": 22,
"year": 2027
},
"company": {
"id": 8,
"name": "Meridian Holdings"
},
"department": null,
"costCentre": {
"id": 14,
"name": "Manchester Clinic"
},
"annualTotal": "128420.50"
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 9
}
}
GET
/budgets/{id}
scope: budgets:read
Fetch one budget
Requires the `budgets:read` scope.
| Parameter | In | What it does |
|---|---|---|
id
required |
path |
Request
curl -s 'https://budget.mushreeves.com/api/v1/budgets/42' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": {
"id": 162,
"name": "Manchester Clinic 2027",
"status": "draft",
"budgetYear": {
"id": 22,
"year": 2027
},
"company": {
"id": 8,
"name": "Meridian Holdings"
},
"department": null,
"costCentre": {
"id": 14,
"name": "Manchester Clinic"
},
"annualTotal": "128420.50"
}
}
GET
/budgets/{id}/lines
scope: budgets:read
List a budget's line items
One row per nominal code, with the twelve original monthly figures and, separately, the reforecast figures. Requires the `budgets:read` scope.
| Parameter | In | What it does |
|---|---|---|
id
required |
path | |
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/budgets/42/lines' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 714,
"description": "Clinical Staff Costs",
"nominalCode": {
"id": 31,
"code": "5001",
"name": "Clinical Staff Costs"
},
"category": {
"id": 4,
"name": "Clinical Staff"
},
"supplier": null,
"expenseOrAsset": "expense",
"annualTotal": "84000.00",
"months": {
"jan": "7000.00",
"feb": "7000.00",
"mar": "7000.00"
}
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 5
}
}
Master data
GET
/companies
List companies
Requires the `master-data:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
includeInactive
|
query | Deactivated records are excluded by default, matching the application. Set true when resolving names on historic rows, which may reference a record that has since been retired. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/companies' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 8,
"name": "Meridian Holdings",
"code": "MER",
"isActive": true
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 1
}
}
GET
/departments
List departments
Requires the `master-data:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
includeInactive
|
query | Deactivated records are excluded by default, matching the application. Set true when resolving names on historic rows, which may reference a record that has since been retired. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/departments' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 8,
"name": "Meridian Holdings",
"code": "MER",
"isActive": true
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 1
}
}
GET
/cost-centres
List cost centres
Requires the `master-data:read` scope. Note that installations may rename "Cost Centre" in the user interface (to "Clinic" or "Site", for example); the API path and field names do not change with it.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
includeInactive
|
query | Deactivated records are excluded by default, matching the application. Set true when resolving names on historic rows, which may reference a record that has since been retired. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/cost-centres' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 8,
"name": "Meridian Holdings",
"code": "MER",
"isActive": true
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 1
}
}
GET
/nominal-codes
List nominal codes
Requires the `master-data:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
includeInactive
|
query | Deactivated records are excluded by default, matching the application. Set true when resolving names on historic rows, which may reference a record that has since been retired. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/nominal-codes' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 31,
"code": "5001",
"name": "Clinical Staff Costs",
"expenseOrAsset": "expense",
"category": {
"id": 4,
"name": "Clinical Staff"
},
"isActive": true
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 19
}
}
GET
/suppliers
List suppliers
Requires the `master-data:read` scope.
| Parameter | In | What it does |
|---|---|---|
page
|
query | 1-based page number. |
perPage
|
query | Rows per page. Values above the maximum are rejected with 400 rather than silently clamped. |
includeInactive
|
query | Deactivated records are excluded by default, matching the application. Set true when resolving names on historic rows, which may reference a record that has since been retired. |
Request
curl -s 'https://budget.mushreeves.com/api/v1/suppliers' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"id": 8,
"name": "Meridian Holdings",
"code": "MER",
"isActive": true
}
],
"meta": {
"page": 1,
"perPage": 25,
"total": 1
}
}
Reports
GET
/reports/variance
scope: reports:read
Budget against actual, with variance
The variance report for one budget year, aggregated by nominal code. Requires the `reports:read` scope. Deliberately not paginated: a variance report is a whole aggregate that only means anything read together, and it is already grouped rather than row-per-transaction. The tighter rate limit is what bounds its cost. Costs only - revenue is excluded, exactly as it is on the equivalent screen. Use the Profit & Loss report if you need revenue.
| Parameter | In | What it does |
|---|---|---|
budgetYearId
|
query | Which budget year to report on. Defaults to the most recent one. A budget year belonging to another organisation returns 404, not 403 - confirming it exists would itself be a leak. |
month
|
query | Report as at the end of this month, 1-12. Defaults to 12, the whole year. Actuals after this month are excluded. |
basis
|
query | Compare actuals against the original budget, or against the reforecast. Defaults to the original. |
companyId
|
query | Restrict to one company |
departmentId
|
query | Restrict to one department |
costCentreId
|
query | Restrict to one cost centre |
nominalCodeId
|
query | Restrict to one nominal code |
categoryId
|
query | Restrict to one budget category |
supplierId
|
query | Restrict to one supplier |
Request
curl -s 'https://budget.mushreeves.com/api/v1/reports/variance' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response
{
"data": [
{
"nominalCode": {
"id": 31,
"code": "5001",
"name": "Clinical Staff Costs"
},
"budget": "141797.76",
"actual": "94551.80",
"variance": "47245.96",
"variancePercent": "33.32",
"rag": "green"
},
{
"nominalCode": {
"id": 44,
"code": "9001",
"name": "Facilities & Rent"
},
"budget": "83026.32",
"actual": "56094.63",
"variance": "26931.69",
"variancePercent": "32.44",
"rag": "green"
}
],
"meta": {
"budgetYear": {
"id": 22,
"year": 2027,
"name": "Budget 2027"
},
"reportingMonth": 6,
"basis": "budget",
"rowCount": 19,
"currency": "GBP",
"note": "Costs only. Revenue is excluded here, as it is on the equivalent screen - use the Profit & Loss report for revenue."
}
}
Rules that apply everywhere
- Every token carries scopes and an expiry, and deactivating its owner invalidates it.
- Every collection is paginated with a maximum; a page size beyond it is a
400rather than a silent clamp. - Money crosses the wire as a decimal string —
"128420.50", never128420.5. Parse it as a decimal, not a float, or you will lose pennies exactly as we would. - Filtering uses named parameters from an allow-list, never a field name taken from the query string.
- A record belonging to another organisation returns
404, not403— confirming that something exists is itself a leak. - Errors share one shape, and the
errorcode is part of the contract. The message beside it may be reworded; the code will not change meaning within a version. - Tolerate unknown fields. A new optional field is an additive change and will arrive without a version bump.
Errors
{
"error": "insufficient_scope",
"message": "This token does not carry the budgets:read scope."
}
| Status | Means |
|---|---|
| 400 | The request itself is wrong — a page size over the maximum, an unparseable filter. |
| 401 | No token, an expired one, or one whose owner has been deactivated. |
| 403 | Authenticated, but the token does not carry the scope this endpoint needs. |
| 404 | No such record — or one that is not yours. The two are deliberately indistinguishable. |
| 429 | Rate limited. Wait for the period given and retry. |
Versioning
The version is in the path from the first release. A new optional field or a new
endpoint is safe within v1; anything that changes the meaning of what is
already there is a new version, announced with a date before the old one stops.