如何连接英国税务海关总局的“数字化纳税”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系统的其他功能基本上都是类似的:提交累计季度数据,触发税务计算,查看计算结果,最后提交最终申报文件。每个步骤都需要使用之前已经通过验证的接口进行操作。
相关文章
为什么绝不应该在客户端代码中嵌入Gemini API密钥(以及Firebase AI逻辑是如何解决这个问题的)
生成式人工智能的快速发展促使成千上万的网页开发者在他们的应用程序中添加智能功能。 人们的第一反应通常是从浏览器直接调用Gemini API的SDK。然而,这种做法存在严重的安全风险:会将你的API密钥暴露给外界。 在本文中,你将了解到为什么将原始的Gemini API密钥提供给客户端是危险的,Firebase AI Logic的代理架构是如何解决这一问题的,以及Firebase App Check又是如何弥补单独使用代理所无法解决的问题。 阅读完本文后,你将能够搭建出一个可正常使用的生产环境配置:一个受到保护的AI Logic客户端、一个配置正确的App Check流程(其中包含调试令牌),以
阅读全文
如何在Flutter中实现LEGO架构[完整手册]
几乎每个人在某个时候都试过把两块乐高积木拼在一起,即使成年后从未拥有过任何一套乐高玩具。你把一块积木压在另一块上面,听到“咔嗒”一声,它们就固定在一起了。 你很可能从未想过这些积木是如何被制造出来的,使用了什么样的塑料材料,又出自哪家工厂。在那一刻,你只关心一件事:那些凸出的连接部分是否对齐了? 这个看似平凡的小动作,其实就是编写这本手册的整个出发点。现在,请暂时把乐高放在一边,想象一下Flutter项目吧。在这个项目中,肯定存在某个文件,团队里的每个人都暗自害怕去打开它。这个文件负责获取数据、对其进行格式化处理、进行验证,最后将结果呈现出来——而所有这些操作都是在一个巨大的`build()`
阅读全文
《设计模式手册:通过C#代码示例学习常见的设计模式》
设计模式是针对软件设计中常见问题的、可复用的解决方案。可以把它们看作是蓝图:并非完整的代码,而是经过验证的模板,你可以根据自己的需求将其调整过来,用于解决自己代码库中的特定问题。 这本手册旨在帮助大家切实理解软件设计模式。我编写这本书是为了所有开发者,无论你使用哪种编程语言。书中的示例是用C#编写的,但这里提到的每一个概念同样适用于Python、Java、TypeScript、Go等语言。 源代码可以在这里找到: github.com/Clifftech123/design-patterns-handbook 。 需要注意的事项: 设计模式本身并不是代码。 它们是一种思考代码结构的方式,是解决
阅读全文
OpenTelemetry的工作原理:一份全面的指南
如果你是一名软件开发人员或DevOps工程师,那么你很可能已经听说过OpenTelemetry。在讨论可观测性、监控或分布式系统的调试时,这个术语经常会被提及。 你可能也知道它的基本定义,但了解OpenTelemetry是什么与真正理解它的运作原理其实是两回事。 读完本指南后,你将能够明白OpenTelemetry是如何从端到端工作的——从请求进入你的应用程序的那一刻起,直到这些数据被显示在可观测性后端系统中。你还会了解到追踪信息、时间跨度、上下文传播机制以及数据导出工具是如何共同构成一个完整的处理流程的。 如果你完全不了解OpenTelemetry,也别担心:接下来的部分会帮助你快速掌握相关
阅读全文