← 文章 / 编程开发
freeCodeCamp 6小时前 · 2026-09-12 08:14:58 · 3 阅读

HMRC Making Tax Digital API 新手入门指南

只要你给英国任何纳税人群体写软件,迟早都得和HMRC(英国税务海关总署)打交道。

个人所得税领域的 Making Tax Digital(数字税务)服务已于 2026 年 4 月 6 日正式上线。目前,年收入超过 5 万英镑的自由职业者和房东已强制要求接入。这一门槛将在 2027 年 4 月降至 3 万英镑,并在 2028 年 4 月进一步降至 2 万英镑,因此未来两年内受覆盖的人口规模大致会翻三倍。

实际影响是,现在有大量小企业需要使用能向 HMRC 报送财务数据的软件,而总得有人去开发这套软件。

第一次翻开 HMRC 的开发者文档时,映入眼帘的全是缩写:MTD、ITSA、OAuth 权限范围(scopes)、防欺诈请求头、义务(obligations)……看起来挺吓人,其中不少细节也确实琐碎。但只要有人按顺序带你走一遍,从零到发出第一个经认证的 API 请求,整个过程比表面看起来要容易得多。

本文正是这样一份指南。我之前为一款 Making Tax Digital 应用构建过 HMRC 集成,接下来我会按自己当时的实操顺序,带你走一遍相同步骤:解释 MTD 是什么、如何创建沙箱应用、OAuth 登录流程的运作机制、那些防欺诈请求头是什么,以及如何发起一次真实的调用。代码示例使用 Node 和 TypeScript,但其中的核心思路适用于任何语言。

HMRC campaign graphic reading Making Tax Digital is here

目录

Making Tax Digital 到底是什么

Making Tax Digital(MTD)是 HMRC 推行的计划,目的是把税务记录和申报迁移到软件中完成。

以往是在网站上填写一年一度的 Self Assessment 大申报表,而 MTD for Income Tax 要求纳税人保存数字化记录,每季度向 HMRC 提交累计的收入和支出数据,年末再做一次最终申报。

(官方的适用范围和时间表,可以参考 GOV.UK 的 MTD for Income Tax 指南。)

HMRC campaign graphic reading Making Tax Digital for Income Tax, one year to go

对开发者来说,关键在于 HMRC 没有提供一个“一键报税”的按钮,而是在 Developer Hub 上提供了一组 REST API:查询某人的业务列表、读取申报义务(什么时候该交什么)、提交季度数据、触发税务计算,等等。

你需要把这些 API 串联成一条完整流程。它们背后都有同样的两道门槛:OAuth 2.0 access token 和一组防欺诈请求头。搞定这两点,剩下的就只是普通的 HTTPS 上的 JSON 请求了。

第 1 步:创建 Sandbox 应用

开发时千万不要直接连真实纳税人数据。HMRC 提供了完整的 sandbox 环境 test-api.service.hmrc.gov.uk,它与生产环境(api.service.hmrc.gov.uk)对应,还允许你创建虚拟纳税人来测试。

具体操作流程如下:

  1. HMRC Developer Hub 上注册一个免费账号。

  2. 创建一个应用,你会拿到 client IDclient secret。secret 要像密码一样对待:放进环境变量,千万别写进源代码。

  3. 设置 redirect URI,这是用户登录后 HMRC 回跳的地址。本地开发用 http://localhost:3000/auth/hmrc/callback 之类的地址就行。之后必须与配置完全一致,一个字符都不能差。

  4. 翻译如下:
  5. 订阅你的应用所需 API。这是新手最容易漏掉的一步。仅仅列出 API 是不够的,你需要逐个点击每个 API 并单独订阅。如果忘了这一步,调用时会直接返回 403 Forbidden,且没有明显的错误提示。

在代码中,沙箱环境和生产环境唯一的区别就是基础 URL,所以建议把这些 URL 放在配置文件中,而不是硬编码:

const config = {
  sandbox: {
    baseUrl: 'https://test-api.service.hmrc.gov.uk',
    authUrl: 'https://test-api.service.hmrc.gov.uk/oauth/authorize',
    tokenUrl: 'https://test-api.service.hmrc.gov.uk/oauth/token',
  },
  production: {
    baseUrl: 'https://api.service.hmrc.gov.uk',
    authUrl: 'https://api.service.hmrc.gov.uk/oauth/authorize',
    tokenUrl: 'https://api.service.hmrc.gov.uk/oauth/token',
  },
};

有一个坑我是吃过亏才学到的:环境变量不要只根据 NODE_ENV 来选择,而是用一个显式的设置。因为你可能需要一个生产部署,但在测试期间仍指向 HMRC 沙箱。如果 URL 只取决于 NODE_ENV,这种组合会强制指向 HMRC 生产环境,并因 client_id is invalid 拒绝你的沙箱凭证。使用一个永远优先的 HMRC_BASE_URL 环境变量可以帮你避免这种混乱。

步骤 2:理解 OAuth Scopes

当你的应用向用户请求访问权限时,你需要指定具体的 scopes(即命名的权限)。对于 MTD 所得税,你需要的是 read:self-assessmentwrite:self-assessment(HMRC 还针对 VAT API 开放了 read:vatwrite:vat)。HMRC 在其 OAuth 2.0 授权指南中记录了这些权限。你需要用空格将它们连接起来:

const scopes = ['read:self-assessment', 'write:self-assessment'].join(' ');

用户会在 HMRC 的同意页面上看到这些权限列表,所以只申请你实际用到的权限。

步骤 3:OAuth 2.0 授权码流程

这是集成的核心,也是标准的三步 OAuth 交互。你可以把它想象成你的应用、用户和 HMRC 之间传递接力棒的过程。全程有四个关键时刻:将用户发送给 HMRC,HMRC 带着 code 将用户送回,你用 code 交换 tokens,之后再刷新这些 tokens。

Sequence diagram of the OAuth flow between the user, your application and the HMRC API

3a. 引导用户跳转至 HMRC

构建授权 URL 并将浏览器重定向到该地址。查询参数包含你的客户端 ID(client ID)、所需权限范围(scopes)、重定向 URI(redirect URI)、response_type=code 以及一个 state 值:

function getAuthorizationUrl(state: string): string {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: hmrcConfig.clientId,
    scope: hmrcConfig.scopes,
    state: state,
    redirect_uri: hmrcConfig.redirectUri,
  });
  return hmrcConfig.authUrl + '?' + params.toString();
}

这个 state 值至关重要。它是防御跨站请求伪造(CSRF)攻击的关键。生成一个随机且不可猜测的字符串,将其存储在服务器端,附加在请求中,并在用户返回时再次校验。如果返回的 state 值与你之前颁发的不一致,立即拒绝该回调。一种常见做法是生成一个 UUID,并将其与用户 ID 和时间戳一起保存:

const state = uuidv4();
await setStateToken(state, { createdAt: Date.now(), userId });
res.redirect(getAuthorizationUrl(state));

接下来,用户在 HMRC 自身的页面上登录(使用其 Government Gateway 凭据)并授权上述权限范围。你的应用无法接触用户的密码,这正是 OAuth 的核心优势所在。

3b. 处理回调

HMRC 会携带两个查询参数 codestate 重定向回你的 redirect URI(若用户拒绝授权,则返回 error)。首先校验 state,然后立即将其删除,使其变为单次有效,防止重放攻击:

const stored = await getStateToken(state);
if (!state || !stored) {
  return redirectError('Invalid or expired authorization request');
}
await deleteStateToken(state); // 单次有效:校验后立即删除

// 可选但明智的做法:让过期请求失效(此处设为 10 分钟)。
if (Date.now() - stored.createdAt > 10 * 60 * 1000) {
  return redirectError('Authorization request has expired. Please try again.');
}

3c. 用 Code 换取 Token

这个 code 有效期很短,单独没什么用。你需要用它通过服务器对服务器的请求,换取 access token 和 refresh token。具体做法是向 token 端点发送一个 POST 请求,参数为 grant_type=authorization_code,使用表单编码,并附上你的 client secret。正因为有 secret,这一步必须放在后端完成,绝不能在浏览器里进行:

async function exchangeCodeForTokens(code: string) {
  const response = await axios.post(
    hmrcConfig.tokenUrl,
    new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: hmrcConfig.clientId,
      client_secret: hmrcConfig.clientSecret,
      redirect_uri: hmrcConfig.redirectUri,
    }).toString(),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
  );

  return {
    accessToken: response.data.access_token,
    refreshToken: response.data.refresh_token,
    expiresIn: response.data.expires_in, // access token 剩余有效秒数
  };
}

把这些 token 存在服务端并与用户关联,永远不要发给客户端。之后所有 API 调用都要靠 access token 来授权。注意 expires_in:HMRC 的 access token 有效期很短(目前是四小时),所以你需要第 3d 步。

3d. 刷新 Token

access token 过期后,不需要让用户重新登录。你可以用 refresh token 换取一对新 token,参数为 grant_type=refresh_token

async function refreshAccessToken(refreshToken: string) {
  const response = await axios.post(
    hmrcConfig.tokenUrl,
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: hmrcConfig.clientId,
      client_secret: hmrcConfig.clientSecret,
    }).toString(),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
  );
  return {
    accessToken: response.data.access_token,
    refreshToken: response.data.refresh_token,
    expiresIn: response.data.expires_in,
  };
}

一个好习惯是:每次调用前先检查已存 token 的过期时间,快过期就主动刷新。HMRC 的 OAuth 文档有完整的生命周期说明。

第 4 步:防欺诈请求头(没人提醒过你的坑)

这里有个反直觉的点:大多数 API 对 Bearer Token 就能满足要求,HMRC 则不然。法律规定,所有 MTD 请求必须附带一组防欺诈请求头。具体来说,就是几十条描述设备、网络路径以及发起请求的软件信息的 Gov-Client-*Gov-Vendor-* 请求头。这能帮助 HMRC 在成千上万的第三方服务商中识别凭证滥用行为。

本指南的目标是建立连接,所以我只做高层介绍:请仔细阅读HMRC 的防欺诈请求头规范,因为规则非常精确,且失败时的表现往往悄无声息。

最关键的决策是连接方式,它由 Gov-Client-Connection-Method 指定。服务器中转的 Web 应用使用 WEB_APP_VIA_SERVER;服务器中转的移动应用使用 MOBILE_APP_VIA_SERVER。这个选择决定了其他请求头哪些是必需的、哪些是禁止的,所以千万别一股脑全发出去。

对初学者来说,真正的好消息是 HMRC 提供了一款测试防欺诈请求头 API。它会检查你的请求,并准确告诉你哪些请求头缺失或格式错误。从第一个提交开始就基于这个校验器开发,能把猜测游戏变成一份检查清单。

想让第一个沙箱调用跑起来,不需要吃透那份规范的所有细节,但还没碰生产数据之前你肯定得把它搞懂。因此,请为此预留充足时间,别拖到最后一刻。

步骤 5:发起第一个已认证请求

现在你有了令牌和请求头,是时候发起一个真实的请求了。除了令牌,每个 MTD 请求还会设置两样东西:一个用于锁定 API 版本的 Accept 请求头,以及前述的防欺诈请求头:

async function request(method, path, accessToken, req, data = null, apiVersion = '2.0') {
  const headers = {
    Authorization: `Bearer ${accessToken}`,
    Accept: `application/vnd.hmrc.${apiVersion}+json`,
    ...hmrcConfig.getFraudHeaders(req),
  };
  // 仅在真正存在请求体时才设置 JSON 内容类型(参见陷阱 3)。
  if (data !== null && data !== undefined) {
    headers['Content-Type'] = 'application/json';
  }
  const res = await axios({ baseURL: hmrcConfig.baseUrl, method, url: path, headers, data });
  return res.data;
}

一个很好的起步请求是“列出此人的业务”,它使用国民保险号(NINO)和 Business Details API。由于该接口为只读操作,用它来验证令牌和请求头是否被接受非常安全:

// GET /individuals/business/details/{nino}/list  (Business Details API v2.0)
const businesses = await request(
  'GET',
  `/individuals/business/details/${nino}/list`,
  accessToken,
  req,
  null,
  '2.0',
);

拿到响应后,下一步自然就是查询义务:哪些季度更新需提交以及何时到期。该接口属于较新的 API 版本,正好可以作为引入下面这些常见陷阱的引子:

// GET /obligations/details/{nino}/income-and-expenditure  (Obligations API v3.0)
const obligations = await request(
  'GET',
  `/obligations/details/${nino}/income-and-expenditure`,
  accessToken,
  req,
  null,
  '3.0',
);

要在测试沙箱中验证这些接口,你可以使用 HMRC 的“创建测试用户”API 来生成一个虚拟纳税人,它会返回一个 NINO 和 Government Gateway 凭据,供你在 OAuth 流程中登录使用。

常见陷阱:可能让你耗费一个下午

初次使用几乎所有人都会踩到以下几个坑:

1. 务必锁定 API 版本,否则会收到 406 错误。

不同的 HMRC API 运行在不同的版本上:Business Details 使用 2.0,Obligations 使用 3.0,而计算类 API 则使用更晚的版本。版本信息存放在 Accept 请求头中(例如 application/vnd.hmrc.3.0+json)。如果发送了错误版本,或者干脆漏掉了这个请求头,HMRC 将返回 406 Not Acceptable。一旦请求莫名其妙地失败,应首先检查版本号是否正确。

2. state 令牌是一次性的。

先验证它,然后立刻删掉,别做其他任何事。如果让它一直留着,就等于削弱了它本应提供的 CSRF 保护。过期的 token 也要及时作废。

3. 无 body 的 GET 请求不要发送 Content-Type: application/json

这一点真的让人意外。HMRC 的边缘节点(CloudFront)会拒绝带 JSON Content-Type 却没有 body 的 GET 请求,返回 403 Bad request,这可能一次性搞挂所有读取类接口。只有在确实有请求体要发送时才设置 Content-Type,就像上面的代码片段那样。

4. 把应用订阅到每个 API。

正如第一步提到的,未订阅的 API 会返回 403,看起来像认证问题,其实不是。

5. 重定向 URI 必须完全一致。

多一个尾部斜杠,或者 httphttps 不匹配,都会在授权页面上报一个莫名其妙的错误。直接复制粘贴,别手动输入。

接下来该做什么

以上就是整个流程的主干:注册 sandbox 应用并订阅,跑一遍 OAuth Authorization Code 流程拿到 token,附上防欺诈请求头,然后发起带版本号的 JSON 调用。

从这里开始,MTD 的其余部分都是类似的操作:提交累计季度数据、触发税务计算、读取结果,最后提交最终申报。每一步都只是另一个接口,需要跨过的仍是那两道你已经越过的关卡。

给新手最中肯的建议是:多用 HMRC 自带的工具——sandbox、测试用户,尤其是防欺诈请求头验证器。把这些都跑出干净的结果,整个集成过程就不再是令人头大的监管迷宫,而更像一个普普通通(只是格外谨慎)的 REST API。

我在做 TapTax,一款面向英国个体经营者的 Making Tax Digital 应用,上面的这些细节正是我在做这个项目时摸索出来的。

关于作者:Solomon Amos 是 TapTax 的创始人,负责搭建其 HMRC Making Tax Digital 集成。过去三年里,他一直担任 HMRC 数字化改造项目的技术架构负责人,拥有工程博士学位,研究方向为机器学习。你可以在 LinkedIn上找到他。

原始来源: freeCodeCamp

评论 (0)