freeCodeCamp.org

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

8.5内容质量
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拒绝

结构提纲

按章节快速跳转。

  1. 说明HMRC API集成所需的一次性设置流程

  2. 解析MTD系统中季度更新的运作原理

  3. 演示如何通过HMRC API查询企业身份信息

  4. 说明如何计算当前应申报的税务周期

  5. 指导创建符合HMRC格式要求的财务数据汇总

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • HMRC MTD API季度更新
    • 技术实现
      • OAuth 2.0授权
      • API版本控制
      • 欺诈预防头
    • 操作流程
      • 沙盒环境设置
      • 企业信息查询
      • 税务周期计算

金句 / Highlights

值得收藏与分享的关键句。

#HMRC API#OAuth 2.0#税务系统#Node.js
打开原文

如何向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),并附加你的防欺诈请求头:

code
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; }

code

`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;

code

该数组位于 `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', );

code

响应结果按企业分类展示义务信息,日期信息嵌套在下一级:

{ "obligations": [ { "typeOfBusiness": "self-employment", "businessId": "XBIS12345678901", "obligationDetails": [ { "periodStartDate": "2026-04-06", "periodEndDate": "2026-10-05", "dueDate": "2026-11-07", "status": "open" } ] } ] }

code

这种嵌套结构在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];

code

在构建此界面之前有三个关键点需要了解:

首先,这些义务没有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 } }

code

分项明细形式会用成本、汽车和货车交通费用、行政成本、专业费用等与自评表对应分类替代该金额。如果某项费用包含个人用途,不可抵扣部分需填写到对应字段:

"periodExpenses": { "costOfGoods": 2100, "carVanTravelExpenses": 1640.2, "adminCosts": 185.99 }, "periodDisallowableExpenses": { "carVanTravelExpensesDisallowable": 410.05 }

code

不能混用这两种形式。如果在任何明细字段中同时提交合并费用(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; }

code

最后,所有金额最多允许两位小数,因此在数据离开代码前请先四舍五入(我使用 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', );

code

成功响应为 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', );

code

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)); } }

code

完整结果包含 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 } } }

code

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