I spent a recent session wiring up close to 20 HMRC Making Tax Digital endpoints — VAT, Income Tax MTD, CIS, Property Business, Partner Income, and a chunk of the legacy PAYE family — against the sandbox environment. The official docs are decent once you're on the right page. Getting to the right page is the hard part.
Here are the specific things that cost real debugging time, in case they save someone else the same hours.
"Charges" isn't a flat resource — it's charges/pensions
If you're using the generic pattern most of HMRC's "Individuals X Income" APIs follow — GET /individuals/{category}/{nino}/{taxYear} — it's tempting to assume every category slots into that same shape. Most do. Charges doesn't.
The Individuals Charges (MTD) API is specifically about pension charges, and the real path has an extra segment:
GET /individuals/charges/pensions/{nino}/{taxYear}
Not /individuals/charges/{nino}/{taxYear}. That extra /pensions/ isn't mentioned anywhere obvious — you find it by actually reading the endpoint list in the Developer Hub docs page for that specific API, not by pattern-matching from a working example elsewhere.
Also worth knowing: this endpoint is versioned separately from most of the income-category family — it needs Accept: application/vnd.hmrc.3.0+json, not the 2.0 that dividends/pensions/foreign income use.
Reliefs isn't one API — it's five
Same trap, worse. There's no single "reliefs" resource at all. The Individuals Reliefs API splits into five genuinely separate sub-resources, each with its own path:
GET /individuals/reliefs/investment/{nino}/{taxYear}
GET /individuals/reliefs/other/{nino}/{taxYear}
GET /individuals/reliefs/foreign/{nino}/{taxYear}
GET /individuals/reliefs/pensions/{nino}/{taxYear}
GET /individuals/reliefs/charitable-giving/{nino}/{taxYear}
If you've built a generic "fetch by category name" tool the way I had, none of the obvious category names (reliefs, relief) will work — you need the actual sub-resource name, which again only shows up if you read the specific API's endpoint list rather than inferring it.
A 404 doesn't always mean "wrong path" — sometimes it means "empty test user"
This one's a genuine trap for anyone testing in sandbox: if you get a 404 on a category you already know works (say, dividends-income, which you tested five minutes ago), and you're now testing it against a different NINO, the 404 might just mean that test user has no seeded data for that category — not that your integration broke.
The fix is boring but important: before debugging a path, re-test a known-good category against the same NINO. If that also fails, it's the test user, not your code.
Sandbox subscriptions are separate from API existence
Several endpoints that returned 404 turned out to be a subscription gap, not a path problem — the app registered in the Developer Hub simply wasn't subscribed to that specific API version. HMRC's sandbox doesn't always distinguish "not subscribed" from "genuinely not found" in its error responses, which makes this one of the more time-costly failure modes to diagnose, since the fix (go subscribe in the Developer Hub) doesn't look anything like what the error message suggests.
Some APIs are gated by a future tax year, and the error doesn't say why
The Individuals Partner Income API — a partner's share of partnership profit, reported through their own Self Assessment — only supports tax years from 2026-27 onward. Test it with 2024-25 (the reasonable default for most other MTD APIs) and you get:
{"code":"RULE_TAX_YEAR_NOT_SUPPORTED","message":"The tax year specified does not lie within the supported range"}
No indication of which years are actually supported. The only way to know is to check the example value HMRC shows in the docs (2026-27) and infer that's the floor — the API itself won't tell you.
Stateful sandbox testing doesn't always require a "real" registered record
One of the more useful discoveries: some stateful APIs (Property Business's period-summary create/fetch cycle, for example) will accept a synthetic ID matching the expected format, even if that ID doesn't correspond to a business that's actually registered anywhere. The sandbox stores it fresh on create and retrieves it on fetch, purely self-contained. Others — CIS Deductions, notably — behave inconsistently: create succeeds, but read/amend/delete against the exact same ID return MATCHING_RESOURCE_NOT_FOUND, which looks like a genuine sandbox bug rather than expected behavior.
The lesson: don't assume all "stateful" APIs behave identically. Test the full create-then-read cycle for each one individually before trusting it.
Quick reference — categories and versions that actually work together
- dividends-income, pensions-income, foreign-income, other-income, insurance-policies-income, employments-income/other, deductions/other — version 2.0
- expenses/other — version 3.0
- charges/pensions — version 3.0
- disclosures, state-benefits — version 2.0
- reliefs/investment, reliefs/pensions, reliefs/foreign, reliefs/charitable-giving, reliefs/other — version 3.0
None of this is criticism of HMRC's APIs — the documentation is genuinely more thorough than a lot of government API docs, and the sandbox stateful-testing model is a good idea when it works consistently. It's just that the path from "read the docs" to "working integration" has more hidden turns than the docs alone suggest, and I didn't find a single place that laid all of this out together. Hopefully this saves someone a few hours.
If you're building against these APIs and hitting your own version of this, I'd genuinely like to compare notes — feel free to reach out.
Top comments (0)