Skip to main content
Budget Tool
← All guides

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:

Download Postman collection OpenAPI specification

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

  1. Import the file. You will get one folder per group of endpoints.
  2. Open the collection's Variables tab.
  3. Set apiToken to a token from Admin → API tokens. Check baseUrl points at the right installation.
  4. 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.

Parameters for GET /budget-years
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.

Parameters for GET /budgets
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.

Parameters for GET /budgets/{id}
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.

Parameters for GET /budgets/{id}/lines
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.

Parameters for GET /companies
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.

Parameters for GET /departments
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.

Parameters for GET /cost-centres
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.

Parameters for GET /nominal-codes
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.

Parameters for GET /suppliers
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.

Parameters for GET /reports/variance
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 400 rather than a silent clamp.
  • Money crosses the wire as a decimal string — "128420.50", never 128420.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, not 403 — confirming that something exists is itself a leak.
  • Errors share one shape, and the error code 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."
}
What each status code means
StatusMeans
400The request itself is wrong — a page size over the maximum, an unparseable filter.
401No token, an expired one, or one whose owner has been deactivated.
403Authenticated, but the token does not carry the scope this endpoint needs.
404No such record — or one that is not yours. The two are deliberately indistinguishable.
429Rate 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.

Guides Get help Accessibility statement Privacy notice