How to Submit a Quarterly Update to HMRC's Making Tax Digital API

TL;DR · AI 摘要
本文详细讲解了如何通过HMRC的Making Tax Digital API提交季度税务更新,涵盖沙盒设置、OAuth授权和API调用步骤。
核心要点
- 使用Node.js和TypeScript实现HMRC API调用需配置Accept头指定API版本(如2.0)
- 未订阅HMRC API会导致403 Forbidden错误,需在开发者门户完成API订阅
- 所有请求必须包含欺诈预防头,否则会被HMRC拒绝
结构提纲
按章节快速跳转。
- §快速回顾
说明HMRC API集成所需的一次性设置流程
解析MTD系统中季度更新的运作原理
演示如何通过HMRC API查询企业身份信息
说明如何计算当前应申报的税务周期
指导创建符合HMRC格式要求的财务数据汇总
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- HMRC MTD API季度更新
- 技术实现
- OAuth 2.0授权
- API版本控制
- 欺诈预防头
- 操作流程
- 沙盒环境设置
- 企业信息查询
- 税务周期计算
金句 / Highlights
值得收藏与分享的关键句。
未订阅API会导致403 Forbidden错误,该错误常被误认为授权问题
欺诈预防头的格式取决于应用的连接方式,需通过getFraudHeaders函数统一生成
HMRC API强制要求Accept头指定版本,错误版本会返回406 Not Acceptable
如何向HMRC的Making Tax Digital API提交季度更新
2026年10月4日
/
#APIs
Solomon Amos
每年四次,所有Making Tax Digital(MTD)个人所得税的个体经营者和房东都必须向HMRC提交收入和支出摘要。
2026-27税务年度的第一个截止日期7月8日已过,第二个截止日期为11月7日。每次提交都通过软件向HMRC发送一次API请求,而正确完成该请求正是本教程的重点。
无需任何前置阅读即可跟随本教程。每个MTD集成都需要一次性的基础设置(沙箱应用、OAuth 2.0访问令牌和防欺诈请求头),以下快速回顾部分将简要说明这些内容,以及每个代码片段使用的辅助请求函数。
如果需要深入了解该设置,我之前在freeCodeCamp教程中已逐步讲解过。
完成本教程后,你将了解如何找到你要申报的企业信息、确定应申报的期间、构建HMRC可接受的累计摘要、提交申报并读取后续的税务计算结果。代码使用Node.js和TypeScript,源自我为Making Tax Digital应用开发的HMRC集成。
目录
- 快速回顾:本教程所依赖的基础设置
- MTD中季度更新的工作原理
- 步骤1:如何查找企业ID
- 步骤2:如何确定应申报内容
- 步骤3:如何构建累计摘要
- 步骤4:如何提交更新
- 步骤5:如何触发并读取税务计算
- 常见问题
- 下一步行动
快速回顾:本教程所依赖的基础设置
在提交任何申报之前,你的应用需要完成与所有MTD集成相同的初始设置。如果你已经完成,可跳至下一章节。否则,以下是简要说明:
- 在HMRC开发者中心注册一个沙箱应用,并订阅本教程中使用的四个API:企业信息、义务、自营业务和个人计算。未订阅的API将返回403 Forbidden错误,这看似授权问题,实则属于未订阅API的错误。
- 使用HMRC的创建测试用户API创建一个沙箱测试用户。该API将为你提供一个带有国民保险号码(NINO)和政府网关凭证的虚拟纳税人。
- 通过HMRC的OAuth 2.0授权码流程获取访问令牌,请求read:self-assessment和write:self-assessment权限范围,并以测试用户身份在HMRC的授权页面登录。
- 在每次请求中发送防欺诈请求头。HMRC强制要求该请求头,具体格式取决于应用的连接方式,因此建议在getFraudHeaders(req)函数中统一生成。
本教程中的每个代码片段都会调用一个小型辅助函数。该函数设置Bearer令牌,通过Accept请求头指定API版本(每个HMRC API都有独立版本,错误版本将返回406 Not Acceptable),并附加你的防欺诈请求头:
import axios from 'axios';
const HMRC_BASE_URL = 'https://test-api.service.hmrc.gov.uk'; // 沙箱环境async function request(method, path, accessToken, req, data = null, apiVersion = '2.0') { const headers = { Authorization: 'Bearer ' + accessToken, Accept: 'application/vnd.hmrc.' + apiVersion + '+json', ...getFraudHeaders(req), }; // Only set a JSON Content-Type when there is a body. HMRC's edge rejects // a bodyless GET that carries one with a 403. if (data !== null && data !== undefined) { headers['Content-Type'] = 'application/json'; } const res = await axios({ baseURL: HMRC_BASE_URL, method, url: path, headers, data }); return res.data; }
`req` 参数是用户的入站请求(在我的代码中是 Express Request),欺诈预防请求头中的客户端信息正是从此处获取。有了令牌和这个辅助函数,你就可以开始提交申报了。
## MTD 中季度更新的工作原理
季度更新不是税务申报。它是一组累计总额:企业在本纳税年度至今赚取和支出的金额,按与自评(Self Assessment)相同的分类进行分组。
关键的词语是“至今”。每次更新都是累计的。它涵盖从纳税年度开始到当前更新周期结束的所有内容,而不仅仅是最近三个月。GOV.UK 关于发送季度更新的指南列出了标准周期及其截止日期:
| 更新周期 | 截止日期 |
|----------------------|--------------|
| 4月6日 至 7月5日 | 8月7日 |
| 4月6日 至 10月5日 | 11月7日 |
| 4月6日 至 1月5日 | 2月7日 |
| 4月6日 至 4月5日 | 次年5月7日 |
这种设计有一个令人愉快的副作用:如果用户发现早期季度的错误,下一次更新只需携带更正即可。HMRC 的端到端服务指南明确指出:每次更新都会使前一次失效,因为每个周期都从4月6日开始。
会计年度从4月1日开始的客户可以选择使用日历周期(4月1日至6月30日等),截止日期相同。你不需要硬编码任何周期,因为义务(Obligations)API 会返回确切日期。
HMRC 将每个必填更新称为“义务(obligation)”:每个自营或房地产业务每年有四个,加上年度税务申报。
以下是即将构建的完整流程:
## 第一步:如何查找企业 ID
每个自营就业端点都需要一个 `businessId`,这是 HMRC 用于标识一个收入来源的标识符。如果独资经营者还出租房产,将拥有两个。你可以通过 Business Details API 使用客户的国民保险号码(NINO)获取它们:
// GET /individuals/business/details/{nino}/list (Business Details API v2.0) const result = await request( 'GET', '/individuals/business/details/' + nino + '/list', accessToken, req, null, '2.0', );
const businesses = result.listOfBusinesses ?? []; const soleTrade = businesses.find((b) => b.typeOfBusiness === 'self-employment'); const businessId = soleTrade?.businessId;
该数组位于 `listOfBusinesses` 下,每个条目包含 `typeOfBusiness`(`self-employment`、`uk-property`、`foreign-property` 或 `property-unspecified`)、`businessId`,以及可选的 `tradingName`。沙盒环境中的自营就业 ID 看起来像 `XBIS12345678901`。
HMRC 的指南建议存储 ID 而不是每次调用前都查找,我也是这样做的。列表很少更改,通常在客户添加或终止业务时才会更改,因此在客户连接时和客户请求时刷新即可。
## 第二步:如何查明应缴事项
// GET /obligations/details/{nino}/income-and-expenditure (Obligations API v3.0) const raw = await request( 'GET', '/obligations/details/' + nino + '/income-and-expenditure?status=open', accessToken, req, null, '3.0', );
响应结果按企业分类展示义务信息,日期信息嵌套在下一级:
{ "obligations": [ { "typeOfBusiness": "self-employment", "businessId": "XBIS12345678901", "obligationDetails": [ { "periodStartDate": "2026-04-06", "periodEndDate": "2026-10-05", "dueDate": "2026-11-07", "status": "open" } ] } ] }
这种嵌套结构在UI中很快会变得难以处理,因此我将其展平为每条义务单独一行,将企业信息字段提升到每行,然后筛选出状态为"open"且截止日期最早的条目:
function flattenObligations(raw, businessId) { return (raw.obligations ?? []) .filter((group) => group.businessId === businessId) .flatMap((group) => (group.obligationDetails ?? []).map((d) => ({ businessId: group.businessId, periodStartDate: d.periodStartDate, periodEndDate: d.periodEndDate, dueDate: d.dueDate, status: (d.status ?? '').toLowerCase() === 'fulfilled' ? 'fulfilled' : 'open', })), ); }
const next = flattenObligations(raw, businessId) .filter((o) => o.status === 'open') .sort((a, b) => a.dueDate.localeCompare(b.dueDate))[0];
在构建此界面之前有三个关键点需要了解:
首先,这些义务没有period键。开始和结束日期定义了期间,且它们正是你在步骤3中需要返回的数据。如果需要稳定的键,请从这两个日期派生一个。
其次,应从响应中读取dueDate而不是自行计算。HMRC设置这些日期并曾进行过修改,因此应让API作为数据来源,而不是在代码中维护日期规则。
第三,过滤条件有特殊规则:fromDate和toDate必须同时发送且间隔不超过366天,businessId过滤器还需要typeOfBusiness字段。如上文所示,通过获取所有open状态数据并在本地代码中过滤,可以规避这些限制。
## 步骤3:如何构建累积摘要
现在来看有效载荷部分。自雇业务API将其称为累积期间摘要,包含四个部分:
- periodDates:必填项。你要申报期间的开始和结束日期,需从义务信息中复制。
- periodIncome:营业额(收入、费用和销售额)、其他业务收入以及从交易收入中扣除的税款。
- periodExpenses:可以是合并费用总额或分项明细。
- periodDisallowableExpenses:分项费用中无法用于抵税的部分。
以下是2026-27财年第二季度的完整有效请求体示例,使用合并费用金额:
{ "periodDates": { "periodStartDate": "2026-04-06", "periodEndDate": "2026-10-05" }, "periodIncome": { "turnover": 28450, "other": 0 }, "periodExpenses": { "consolidatedExpenses": 4310.45 } }
分项明细形式会用成本、汽车和货车交通费用、行政成本、专业费用等与自评表对应分类替代该金额。如果某项费用包含个人用途,不可抵扣部分需填写到对应字段:
"periodExpenses": { "costOfGoods": 2100, "carVanTravelExpenses": 1640.2, "adminCosts": 185.99 }, "periodDisallowableExpenses": { "carVanTravelExpensesDisallowable": 410.05 }
不能混用这两种形式。如果在任何明细字段中同时提交合并费用(consolidatedExpenses),系统将返回 RULE_BOTH_EXPENSES_SUPPLIED 错误。由于每个不可抵扣字段都对应一个明细分类,不可抵扣费用必须与明细表单一起提交。
那么在什么情况下允许使用合并表单?HMRC 的服务指南指出,年营业额低于 9 万英镑的客户可以报告单一费用总额。由于每次更新都是累计的,实际检查方式是在每次提交时将当年累计营业额与阈值进行对比。一旦营业额达到 9 万英镑,更新必须采用明细形式。我会在任何数据发送至 HMRC 前在服务器端执行此检查:
const CONSOLIDATED_EXPENSES_TURNOVER_LIMIT = 90_000;
function checkConsolidatedExpensesLimit({ turnover, usesConsolidatedExpenses }) { if (!usesConsolidatedExpenses) return null; if ((turnover ?? 0) >= CONSOLIDATED_EXPENSES_TURNOVER_LIMIT) { return ( '当营业额达到 £90,000 时,不允许使用合并费用。' + '请改用明细方式填写您的费用。' ); } return null; }
最后,所有金额最多允许两位小数,因此在数据离开代码前请先四舍五入(我使用 Number(total.toFixed(2)) 实现)。
## 第 4 步:如何提交更新
构建好请求体后,提交操作本身只需一次 PUT 请求。税务年度以 YYYY-YY 格式放在路径中,期间日期则包含在请求体中而非 URL 中:
// PUT /individuals/business/self-employment/{nino}/{businessId}/cumulative/{taxYear} // Self Employment Business API v5.0 await request( 'PUT', '/individuals/business/self-employment/' + nino + '/' + businessId + '/cumulative/' + taxYear, accessToken, req, summary, '5.0', );
成功响应为 204 No Content,不包含收据编号,因此请自行保留发送内容、时间和 HMRC 返回状态的审计记录。您随时可通过相同路径的 GET 请求读取 HMRC 存档的副本。
由于这是 PUT 请求,相同调用既可用于创建也可用于修改。对同一税务年度重新提交将替换 HMRC 当前保存的数据。这是累积模型按设计工作的体现,也是该流程中最昂贵的错误来源(详见注意事项)。
此接口仅接受从 2025-26 财政年度开始的税务年度。早期教程中向 /period 端点发送 POST 请求描述的是更早的按期间模型。
## 第 5 步:如何触发和读取税务计算
更新后,客户通常希望了解大致的税务金额。Individual Calculations API 通过异步方式回答这个问题:您触发计算后会获得一个 ID,稍后可获取结果。计算类型位于路径末尾,而 in-year 是季度更新后需要使用的类型:
// POST /individuals/calculations/{nino}/self-assessment/{taxYear}/trigger/in-year // Individual Calculations API v8.0. 返回 202 Accepted 和 calculationId const { calculationId } = await request( 'POST', '/individuals/calculations/' + nino + '/self-assessment/' + taxYear + '/trigger/in-year', accessToken, req, {}, // 一个空对象,而非 null(详见注意事项) '8.0', );
HMRC 的接口文档建议在尝试获取结果前至少等待五秒钟。在计算完成之前,获取接口会返回 404 Not Found,因此使用一个简短且有限重试循环可以处理这种情况:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForCalculation(nino, taxYear, calculationId, accessToken, req) { const path = '/individuals/calculations/' + nino + '/self-assessment/' + taxYear + '/' + calculationId; await sleep(5000); // HMRC 建议在触发后至少等待五秒钟
for (let attempt = 0; attempt < 5; attempt += 1) { try { return await request('GET', path, accessToken, req, null, '8.0'); } catch (err) { // 仅当返回 404 时表示"尚未就绪"。其他任何错误都是真实错误,应重新抛出。 if (err.response?.status !== 404 || attempt === 4) throw err; } await sleep(1000 * (attempt + 1)); } }
完整结果包含 HMRC 知悉的所有收入类型,但季度更新界面只需少数字段。以下是包含示例数据的精简响应:
{ "metadata": { "calculationId": "f2fb30e5-4ab6-4a29-b3c1-c7264259ff1c", "taxYear": "2026-27", "calculationType": "in-year", "periodFrom": "2026-04-06", "periodTo": "2026-10-05" }, "calculation": { "taxCalculation": { "totalIncomeTaxAndNicsDue": 3902.6 } } }
calculation.taxCalculation.totalIncomeTaxAndNicsDue 是核心数据。metadata.periodTo 显示数据覆盖的年度范围:最新提交的截止日期,而非用户请求的日期。
如果提交的数据未通过 HMRC 的校验,将完全不会生成计算结果。此时 messages.errors 会包含 { id, text } 对象列表,应将这些文本展示给用户以便其修正记录。
在展示任何年度数据前,根据 HMRC 的最低功能标准要求必须添加免责声明。HMRC 服务指南的税务计算部分提供了标准措辞,您也可以自行撰写:
> "此计算仅基于 HMRC 截至 20XX-XX-XX 收到的关于您收入和支出的信息。随着我们在税务年度内收到更多关于您的信息,这些数据可能会发生变化。"
请将日期替换为 periodTo 字段的值。同时建议说明这是预估数据:HMRC 的指南明确指出用户在此阶段无需支付任何费用。
## 常见陷阱
### 1. 沙箱默认数据年份严重过时
在沙箱中调用义务 API 且不指定测试场景时,会返回旧税年的静态数据,累计接口会以 RULE_TAX_YEAR_NOT_SUPPORTED 拒绝这些数据。应改用 Gov-Test-Scenario: DYNAMIC 获取当前年度的开放义务。
累计接口默认也不保持状态:PUT 操作后执行 GET 会返回预设数据。如需完整往返测试,请使用 STATEFUL 场景并配合通过自评测试支持 API 创建的企业。
### 2. 新更新会覆盖旧数据
由于每次 PUT 操作都会替换年度数据,空白表单仅提交用户输入内容可能导致覆盖先前季度数据。应从获取接口或自身记录中预填充数据,确保每次重新提交都从完整年度累计数据开始。
### 3. 需提交零值,不可遗漏字段
HMRC的接口文档说明提交时必须包含收入和支出数值,即使为零,因此即使某个季度没有收入,仍需将营业额和其他字段设为0。GOV.UK也指出,当季度内未发生任何变动时,客户仍需提交更新,因此不要因为所有数值均为零而阻止提交。
### 4. 提交时间不可过早
累计接口会拒绝在季度结束前10天以上提交的申报(错误码:RULE_EARLY_DATA_SUBMISSION_NOT_ACCEPTED),以及提交的结束日期早于已提交记录的申报(错误码:RULE_SUBMISSION_END_DATE_CANNOT_MOVE_BACKWARDS)。请明确显示季度结束日期,并仅在日期适用时启用提交按钮。
### 5. "已完成"状态可能需要一小时
成功收到204响应后,HMRC可能需要最多一小时才会将义务标记为已完成,因此立即重新查询仍会显示为"未完成"。请信任您自己的提交记录,并告知客户状态将在不久后更新。
### 6. 使用空对象触发计算
在测试中,使用null作为触发请求体时,HMRC的计算后端会返回500错误,而使用空JSON对象({})则被接受。注意版本差异:Individual Calculations 9.0目前仅在沙箱环境中可用,但截至撰写时生产环境可用的版本为8.0。
## 下一步行动
您现在已掌握完整的季度申报流程:找到企业,读取其未完成的义务,构建符合合并支出规则的累计摘要,提交后触发计算,等待结果并读取带有适当免责声明的计算结果。
在第四次更新后,年度结算仍通过Individual Calculations API完成:您将触发最终结算计算而非年度内计算,向客户展示结果后,再基于该计算ID提交最终声明。
这需要单独的教程说明,这也是我计划接下来撰写的部分。
在规划产品上线前,请注意:HMRC的API页面目前显示已停止接受2026-27季度更新产品的生产环境凭证申请。沙箱环境仍保持开放,您现在即可构建和测试上述所有功能,但在确定上线日期前,请务必阅读Self Employment Business API页面上的相关通知。
我是TapTax的联合创始人,这是一款面向英国自由职业者的Making Tax Digital应用,本文中的代码均来自该项目。
关于作者:Solomon Amos是TapTax的创始人,负责构建其HMRC Making Tax Digital集成。他过去三年担任HMRC数字化转型项目的首席技术架构师,拥有工程学博士学位,研究领域包括机器学习。您可以在LinkedIn上找到他。
TapTax创始人,为英国自由职业者开发Making Tax Digital应用,我在此构建了HMRC集成:OAuth、防欺诈请求头、季度更新和最终申报。过去三年担任HMRC数字化转型项目的首席技术架构师,拥有工程学博士学位,研究方向为机器学习。曾于约克大学担任应用人工智能讲师。我撰写关于HMRC API开发的相关内容。
如果您读到了这里,请感谢作者以表达您的支持。说声谢谢
免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成为开发者。立即开始
ADVERTISEMENT