如何连接英国税务海关总局的“数字化纳税”API:初学者指南
如果你为那些需要缴纳英国税款的用户开发软件,那么迟早你会需要与 HMRC 进行沟通。 针对所得税的“数字化纳税”计划于2026年4月6日正式实施,目前自我雇用人以及年收入超过50,000英镑的房东必须遵守这一规定。到2027年4月,这一收入门槛将降至30,000英镑;而到2028年4月,则进一步降低到20,000英镑。因此,在未来两年内,受该规定影响的人群数量将会大幅增加。 实际上,这意味着许多小型企业现在都需要能够将其财务数据发送给HMRC的软件,而有人就需要开发这样的软件。 当你第一次阅读HMRC提供的开发者文档时,会看到一大堆缩写词:MTD、ITSA、OAuth权限范围、防欺诈头部信息以
如果你为那些需要缴纳英国税款的用户开发软件,那么迟早你会需要与HMRC进行沟通。
针对所得税的“数字化纳税”计划于2026年4月6日正式实施,目前自我雇用人以及年收入超过50,000英镑的房东必须遵守这一规定。到2027年4月,这一收入门槛将降至30,000英镑;而到2028年4月,则进一步降低到20,000英镑。因此,在未来两年内,受该规定影响的人群数量将会大幅增加。
实际上,这意味着许多小型企业现在都需要能够将其财务数据发送给HMRC的软件,而有人就需要开发这样的软件。
当你第一次阅读HMRC提供的开发者文档时,会看到一大堆缩写词:MTD、ITSA、OAuth权限范围、防欺诈头部信息以及各种相关要求。这些内容看起来令人生畏,而且其中很多细节确实比较繁琐。但是,如果在有人一步步地指导你的话,从零开始实现第一个经过身份验证的API调用其实并没有那么困难。
这本指南正是为了帮助大家解决这些问题而编写的。我曾经为一个“数字化纳税”应用开发过与HMRC接口连接的代码,现在我将按照自己当初的操作步骤,依次向你们讲解每一个环节:什么是“数字化纳税”、如何获取沙箱测试环境、OAuth登录流程的具体工作原理、那些防欺诈头部信息的作用,以及如何进行真正的API调用。虽然所使用的编程语言是Node.js和TypeScript,但这些知识其实适用于任何编程语言。
目录
什么是“数字化纳税”
“数字化纳税”是HMRC推出的一项计划,旨在将税务记录的保存和申报工作转移到软件系统中来完成。
与以往每年都需要在网站上填写一份复杂的自我评估申报表不同,“数字化纳税”要求人们保留电子化的财务记录,并且每个季度都要向HMRC提交一次收入和支出的累计更新信息,最后在年底再进行一次最终申报。
(有关官方的范围和时间安排,请参阅GOV.UK关于所得税数字化申报的指南。)
在开始开发之前,千万不要直接使用真实的纳税人数据。英国税务海关总署在test-api.service.hmrc.gov.uk这个地址上提供了一个完整的沙箱环境,该环境与生产环境(api.service.hmrc.gov.uk)是完全相同的,你可以利用这个沙箱环境来创建虚拟的纳税人数据进行测试。
具体的操作步骤如下:
在英国税务海关总署开发者平台上注册一个免费账户。
创建一个应用程序。你会得到一个客户端ID和一个客户端密钥。请将这个密钥视为密码一样加以保管,绝对不能将其写入源代码中。
设置一个重定向URL。这是用户在登录后会被引导回的网址。在本地测试时,使用像
http://localhost:3000/auth/hmrc/callback这样的地址就可以了。这个URL以后必须与实际使用的URL完全一致,不能有任何字符上的差异。为你的应用程序订阅所需的API服务。这是初学者经常忽略的步骤。仅仅在列表中勾选这些API是不够的,你需要逐一点击每个API并进行订阅操作。如果忘记了这个步骤,后续的调用就会遇到
403 Forbidden错误,而且没有任何明显的提示原因。
在代码中,沙箱环境和生产环境之间唯一的区别就在于基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变量。在测试阶段,你可能仍然需要使用沙箱环境,而如果你的URL地址是根据NODE_ENV来决定的,那么当正式进入生产环境时,系统会因为URL不匹配而拒绝你的沙箱访问权限,错误信息会是“client_id is invalid”。因此,设置一个固定的HMRC_BASE_URL环境变量可以避免这种麻烦。
步骤2:了解OAuth权限范围
当你的应用程序请求用户授权时,它会请求特定的权限范围,这些权限范围实际上就是被赋予的应用程序功能。对于MTD所得税应用来说,你需要的权限范围是read:self-assessment和write:self-assessment;而HMRC也为增值税相关API提供了read:vat和write:vat这些权限。HMRC在它的OAuth 2.0授权指南中详细说明了这些权限范围。你需要用空格将它们连接起来,如下所示:
const scopes = ['read:self-assessment', 'write:self-assessment'].join(' ');
用户在HMRC的授权页面上会看到这些权限范围的详细列表,因此请只请求那些你的应用程序真正需要的权限。
步骤3:OAuth 2.0授权码流程
这是整个集成过程的核心部分,它遵循标准的 OAuth三方交互机制。你可以将这个过程想象成你的应用程序、用户以及HMRC之间互相传递“接力棒”的过程。整个流程包含四个关键环节:首先将用户引导至HMRC的授权页面,然后HMRC会返回一个授权码,接着你用这个授权码去换取访问令牌,最后需要定期更新这些令牌以保持其有效性。
3a. 将用户引导至HMRC的授权页面
你需要生成一个授权URL,并将用户的浏览器重定向到该地址。查询字符串中会包含你的客户端ID、所请求的权限范围、重定向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值非常重要,它是防止跨站请求伪造攻击的关键。你需要生成一个随机且无法被猜测的字符串,在服务器端保存它,并将其添加到授权URL中;当用户返回时,你需要再次检查这个值。如果返回的值与你之前生成的值不一致,就需要拒绝用户的回调请求。一种常见的做法是生成一个UUID,并将其与用户的ID和时间戳一起存储起来:
const state = uuidv4();
await setStateToken(state, { createdAt: Date.now(), userId });
res.redirect(getAuthorizationUrl(state));
之后,用户需要使用自己的政府门户账号在HMRC的官方网站上登录,并确认所请求的权限范围。你的应用程序根本不会看到用户的密码,而这正是OAuth机制的核心所在。
3b. 处理回调请求
英国税务海关总署会将用户重定向到您指定的重定向URI,并附带两个查询参数:code和state;如果用户拒绝了请求,还会传递一个error参数。首先需要验证state参数的值,然后立即将其删除,以确保该参数不会被再次使用:
const stored = await getStateToken(state);
if (!state || !stored) {
return redirectError('无效或已过期的授权请求');
}
await deleteStateToken(state); // 一次性使用:在验证通过后立即删除该令牌
// 可选但明智的做法是:使过期的请求失效(这里设置为10分钟)。
if (Date.now() - stored.createdAt > 10 * 60 * 1000) {
return redirectError('授权请求已过期。请重新尝试。');
}
3c. 用代码兑换令牌
code这种代码的有效期很短,单独使用是没有意义的。你需要通过服务器之间的交互,用它来换取访问令牌和刷新令牌。具体操作是向令牌端点发送一个POST请求,其中设置grant_type=authorization_code,并且需要将你的客户端密钥以表单编码的形式一起发送。之所以要在后端进行处理而不在浏览器中完成这个步骤,原因就在于这个客户端密钥的存在:
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,
expires_in: response.dataexpires_in, // 访问令牌的有效期,单位为秒
};
}
这些令牌应该存储在服务器端,并与特定用户关联起来,绝对不能发送给客户端。访问令牌是用于授权后续所有API请求的关键。需要注意的是expires_in:HMRC提供的访问令牌有效期很短(目前为4小时),因此你需要执行第3d步操作。
3d. 刷新令牌
当访问令牌过期时,不需要让用户重新登录。你可以使用刷新令牌来获取新的访问令牌和刷新令牌,具体操作是发送一个带有grant_type=refresh_token的POST请求:
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,
refresh_token: response.data.refresh_token,
expires_in: response.dataexpires_in,
};
}
一个好的做法是在每次调用API之前检查存储的令牌是否即将过期,如果快要过期了,就主动进行刷新。HMRC的OAuth文档中详细介绍了令牌的整个生命周期。
第4步:防欺诈请求头信息(这部分内容很少有人会提醒你)
这里有个令人意外的地方:大多数API都接受“bearer token”这种认证方式,但HMRC却不这么认为。根据法律规定,HMRC要求你的软件在每次进行MTD请求时,都必须发送一组防欺诈头部信息;这些信息包括几十个Gov-Client-*和Gov-Vendor-*头部字段,它们用于描述发起请求的设备、网络路径以及所用软件。这些信息有助于HMRC检测数千家第三方供应商是否存在滥用认证凭证的行为。
本指南主要是介绍如何建立连接,因此我会保持说明的简洁性:请仔细阅读HMRC关于防欺诈头部信息的规范,因为这些规定非常详细,而且违规行为往往不会产生明显的错误提示。
其中最重要的决策就是连接方式,这一信息需要通过Gov-Client-Connection-Method头部字段来指定。如果应用程序是通过服务器进行调用的,那么应使用WEB_APP_VIA_SERVER;如果是移动应用,则应使用MOBILE_APP_VIA_SERVER。选择正确的连接方式会决定哪些头部信息是必须发送的,哪些是被禁止的,因此千万不要随意发送所有可用的头部信息。
对于初学者来说,一个真正的好消息是:HMRC提供了防欺诈头部信息检测API,该工具可以检查你的请求内容,并告诉你哪些头部信息缺失或格式不正确。从第一次提交代码开始,就可以利用这个验证工具来确保你的请求符合规范,这样原本需要靠猜测的工作就能变成有依据的检查流程。
在初次进行测试时,你可能不需要完全掌握那些规范的详细内容,但在开始处理生产环境的数据之前,就必须彻底理解这些规范。因此,请尽早安排时间学习这些内容,而不要等到最后才去处理。
步骤5:进行第一次经过身份验证的请求
现在你已经获得了token和所需的头部信息,是时候发出真正的请求了。每次进行MTD请求时,除了token之外,还需要设置两个额外的字段:Accept头部字段用于指定API版本,而那些防欺诈头部信息也是必须发送的。
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),
};
// 只有当请求体存在时,才需要设置Content-Type字段
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。由于这个操作只是读取数据,因此这是一个安全的方式,可以用来确认你的token和头部信息是否被正确接受。
// 使用Business Details API v2.0查询某人的企业信息
const businesses = await request(
'GET',
`/individuals/business/details/${nino}/list`,
accessToken,
req,
null,
'2.0',
);
一旦获得了这些信息,接下来自然要查看的义务相关数据:哪些季度更新报告应该提交,以及提交的时间。这个功能的实现依赖于较新版本的API,这也是了解其中可能存在的问题的绝佳切入点:
// 使用Obligations API v3.0获取{nino}对应的收支详情
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使用不同的版本:企业信息相关接口使用2.0版本,义务相关接口使用3.0版本,而计算相关接口则使用更高级的版本。版本信息位于Accept头部字段中(例如application/vnd.hmrc.3.0+json)。如果发送了错误的版本号,或者忘记了设置这个头部字段,HMRC会返回406 Not Acceptable错误响应。当某个请求突然失败时,首先检查一下使用的API版本是否正确。
2. state令牌是一次性使用的。
在使用完这个令牌后,必须立即将其删除,否则会削弱它原本所提供的CSRF防护功能。过期的令牌也应当及时清除。
3>在没有请求体的GET请求中,不要设置Content-Type: application/json。
这个错误真的会让人感到意外。HMRC的边缘服务器(CloudFront)会拒绝那些没有请求体但设置了Content-Type: application/json的GET请求,并返回403 Bad request错误,这可能会导致所有的数据读取接口都无法正常工作。只有当确实需要发送请求体时,才应该设置Content-Type字段。
4>你的应用程序需要为每个API分别进行订阅操作。
如第一步所述,如果未对某个API进行订阅,尝试调用该接口时会收到403错误响应。这个错误看起来像是与认证相关的问题,但实际上并非如此。
5>重定向URL必须完全匹配才能正常工作。
如果重定向URL的末尾带有斜杠,或者http和https>协议不匹配,都会在同意页面上显示错误信息。请直接复制重定向URL,而不要重新输入。
下一步该怎么做
整个操作流程可以概括为以下几点:注册一个沙箱应用程序并完成相关订阅设置,运行OAuth授权流程以获取令牌,添加防欺诈相关的头部信息,然后使用指定版本的API发送JSON格式的请求。
从这一步开始,MTD系统的其他功能基本上都是类似的:提交累计季度数据,触发税务计算,查看计算结果,最后提交最终申报文件。每个步骤都需要使用之前已经通过验证的接口进行操作。
相关文章
如何将应用程序从AWS迁移到GKE [完整指南]
这个指导性实验将带你了解如何迁移一个小型支付应用。该应用包含使用React开发的用户界面以及运行在AWS EC2实例上的Express API,而数据库则仍保存在AWS上。 你需要将用户界面和API迁移到Google Kubernetes Engine上,但无需更改客户目前使用的公共URL地址,而是逐步将流量从AWS转移至GKE,每次转移的比例为几百分比。 完成整个迁移过程后,你将能够在本地运行该应用,在AWS上搭建基础设施,通过八个Terraform步骤在GKE上构建环境,测试新环境是否正常工作,然后将生产环境的流量全部转移到GKE上,完成切换流程,并拆除不再需要的资源。 下图展示了迁移过程
阅读全文
由 TypeScript 7 提供支持的 Angular 编译器已进入测试阶段,该编译器配备了基于 Rust 的预处理工具。
Angular团队推出了ngp这一编译器预处理器,该工具利用Oxc重新实现了Rust语言中的分析阶段处理流程。它取代了ngc对Transformer API的依赖——而TypeScript 7.0已经移除了对Transformer API的支持。早期的测试结果显示,使用ngp进行冷构建时,编译速度可以提高2到3倍。预计这一新技术将在Angular 22的次要版本中正式被采用;在此之前,生产环境仍然会继续使用ngc进行编译。 作者:Daniel Curtis
阅读全文
谷歌云的支出上限功能现已通过API接口正式推出,并允许用户每月进行重置设置。
Google Cloud现已全面推出了针对云账单预算的使用额度限制功能:一旦某个项目的月度预算被用完,相关服务的新增使用请求将会被暂停,但现有资源不会被删除。该系统提供了API接口、预估费用功能以及自动每月重置机制;不过这些额度限制仅适用于Gemini API、Agent Platform、Cloud Run以及Cloud Run Functions等服务。在限制措施生效期间,如果仍出现超支情况,相关费用仍然会正常计费。 作者:Steef-Jan Wiggers
阅读全文
如何在React Native应用程序中使用shadcn/ui[完整使用指南]
如果你已经开发过几个React Native应用,那么以下这些情况你应该很熟悉。你通常会从创建一个`colors.ts`文件和一个简单的`Button`组件开始。一开始,一切看起来都很正常。但后来,当设计发生了一些变化时,那个小小的颜色配置文件就会逐渐变长;而每次有人要求稍微调整组件的样式时,`Button`组件也需要添加更多的属性来满足这些新需求。 刚开始的时候,这种开发方式确实挺方便的,但经过几次设计修改后,即使是小小的更新,也可能意味着需要检查项目中的多个文件。 过了一段时间后,典型的颜色配置文件会变成这样: export const colors = { primary: '#6e48
阅读全文