幂等验证请求:避免重复检查和重复收费

Charles Archibong, 联合创始人
为每个逻辑验证分配一个请求 ID,在发送请求之前保存该 ID,并在每次重试时重复使用该 ID。重复的请求 ID 会返回已存在的验证,而不是创建并收取第二个验证的费用。

要点
- 重复的 metadata.requestId 会返回现有的验证信息,而不是创建一个新的验证信息。
- 在第一次尝试之前,从您自己的记录中生成 requestId 并将其存储起来,而不是在重试循环中存储。
- 对 429、502、503、504 和连接失败的情况进行回退重试;将其他 4xx 响应视为需要修复的错误。
- 返回 202 仅表示已排队。验证失败是需要处理的结果,而不是需要重试的错误。
为了安全地重试验证请求,请发送相同的metadata.requestId每次重试都是如此。 Toden 款待requestId作为你的幂等键:如果请求到达时带有requestId贵组织已经使用过的 API 返回现有验证信息,而不是创建第二个验证信息,这样您就不会被收取两次费用。
只有正确处理,这一个字段才能保护你。requestId每次逻辑验证都需要生成一次验证码,在第一次尝试之前保存,并原封不动地重复使用。在重试循环中生成验证码,每次重试都会成为一次全新的验证。本文的其余部分将介绍验证码的创建位置、重试响应以及哪些“失败”实际上并非失败。
如果没有幂等性,会发生什么问题?
典型的例子是成功后超时。您的服务器发送POST /api/kyc/verify, 这 API 接受请求并将验证加入队列,但响应在返回途中丢失:可能是移动网络连接中断、负载均衡器超时或 Pod 重启。从您这边来看,请求失败;从我们这边来看,请求成功。
如果您的代码使用新的标识符重试,平台会检测到新的请求并创建第二次验证。根据环境文档 (在新标签页中打开),在生产环境中,每次完成的验证都会从您的信用余额中扣款,因此丢失的响应会导致您损失一次验证费用。此外,这还会导致同一申请人关联两个验证 ID,您的对账流程现在必须确定哪个 ID 才是真实的。
与此相同requestId重试会返回原始验证结果,丢失的响应不会造成任何损失。
验证端点上的 requestId 是如何工作的?
它存在于请求中metadata对象。来自创建验证参考 (在新标签页中打开):
{
"country": "NG",
"idType": "bvn",
"idNumber": "12345678901",
"externalUserId": "user_42",
"metadata": { "requestId": "order_1001", "loanId": "loan_20191" }
}首次提交返回202 Accepted:
{
"verificationId": "ver_01j9...",
"status": "processing",
"externalUserId": "user_42",
"metadata": { "loanId": "loan_20191" }
}后来提交的版本也相同requestId返回该验证结果,无论它处于何种状态,例如{ "verificationId": "ver_01j9...", "status": "approved" }。externalUserId并且您的元数据与存储的完全一致,因此重试结果与原始结果一致。
密钥的权限范围仅限于您的组织。如果requestId与属于另一个组织的某个人发生碰撞, API 返回403和{ "error": "Forbidden" }而不是透露任何有关核实的信息。
请求 ID 应该在哪里生成?
从你自己的记录来看,在你发送任何内容之前,有一个普遍适用的模式:
当客户开始身份验证时,在您自己的数据库中为该尝试(申请、注册步骤、贷款)创建一行。
推导
requestId例如,从那一行开始。kyc-application-8841并将其存储在行中。发送包含该存储值的请求。
每次重试时,都从行中读取该值。不要重新计算该值。
以下三条规则适用:
不要使用随机数 UUID 每生成 HTTP 调用。这是调用的唯一标识符,这与您需要的恰恰相反。
命名空间。一个裸露的
1001随着时间的推移,它很可能会与您自己的其他记录发生冲突,并且跨组织冲突会再次发生。403前缀,例如kyc-application-使该值明确无误。只有当真正需要重新验证时才创建新的验证码。如果客户被拒绝,而您决定允许他们从头开始重新申请,那么这就是一次新的逻辑验证,需要使用新的验证码。
requestId重复使用旧版本只会返回验证失败的结果。
如果审核员发送验证信息以便申请人可以重做某些步骤,则无需新的验证。requestId完全不会。已发送或再次验证的验证信息会保留其 ID,重新提交的信息将显示为新的验证信息。attempt如验证结果参考文献 (在新标签页中打开)所述。
当 SDK 为您提交
大多数集成不会调用/verify直接。当申请人点击提交按钮时,Web、React Native 和 Flutter SDK 会发出请求。它们接受metadata你的requestId(metadata={{ requestId: "order_1001" }}在 React Native 中,metadata: {'requestId': 'order_1001'}在 Flutter 中),所以同样的原则也适用:传递从你自己的记录中派生的值,而不是每次屏幕挂载时都重新生成的值。
哪些回复应该重试?
错误文档 (在新标签页中打开)写得很清楚,并且对应到一个简短的表格中。
回复 | 重试? | 该怎么办 |
|---|---|---|
| 不 | 完成。跟踪 |
| 是的 | 等待 |
| 是的 | 带抖动的指数退避,相同 |
连接重置, DNS 失败,超时 | 是的 | 同上。请求可能已经成功,这正是为什么 |
| 并非盲目 | 请保留请求详情,如果问题仍然存在,请联系客服。 |
| 不 | 修复有效载荷。读取 |
| 不 | 检查密钥及其环境。 |
| 不 | 生产需要经过审核的企业简介。 |
| 不 | 你的 |
官方记录的限制是每个客户端每 15 分钟 100 个请求,RateLimit-Limit,RateLimit-Remaining和RateLimit-Reset读取响应头。RateLimit-Remaining允许批量作业在被拒绝之前(而不是之后)减慢速度。速率限制页面 (在新标签页中打开)指出,沙盒环境和生产环境的速率限制可能有所不同,并且可以针对您的帐户进行调整。
安全的重试循环是什么样的?
一个最小化的 Node.js 版本,使用全局fetch数据体在循环之外只序列化一次,因此每次尝试发送的字节都相同。requestId。
const VERIFY_URL = 'https://api.toden.co/api/kyc/verify';
const RETRYABLE = new Set([429, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoff = (attempt) => Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random());
export async function submitVerification(body, apiKey, maxAttempts = 5) {
if (!body.metadata?.requestId) throw new Error('metadata.requestId is required');
const payload = JSON.stringify(body);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let res;
try {
res = await fetch(VERIFY_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: payload,
});
} catch (networkError) {
if (attempt === maxAttempts) throw networkError;
await sleep(backoff(attempt));
continue;
}
if (res.ok) return res.json(); // 202 for a new verification, or the existing one on a replay
if (!RETRYABLE.has(res.status) || attempt === maxAttempts) {
const detail = await res.json().catch(() => ({}));
throw Object.assign(new Error(detail.error ?? `HTTP ${res.status}`), { status: res.status, detail });
}
const resetSeconds = Number(res.headers.get('RateLimit-Reset'));
await sleep(res.status === 429 && resetSeconds > 0 ? resetSeconds * 1000 : backoff(attempt));
}
}注意缺少的内容:无重试400,401或者403并且任何地方都没有新的标识符。调用者提供了一个requestId它已经存储好了。
验证失败是否需要重试?
不,混淆这两者是造成重复数据的常见原因。202仅表示请求已排队。验证仍可能结束。declined因为人脸不匹配,或者因为政府数据库中找不到对应的身份证件。这些结果通过 webhook 或轮询方式提供,每种方式都有其自身的信息。reason以及稳定的reasonCode他们不是 HTTP 错误。
如果你的重试逻辑会监控结果并在验证失败后重新提交,那么它要么会收到相同的被拒绝验证结果(相同的验证结果)。requestId或者创建第二个付费验证,验证同一个错误输入(新)。requestId两者都无济于事。继续往下看。reasonCode改为:要求提供一张新照片document_blurry,以获得更正后的数字identity_not_found并故意让该人重新进行验证。
webhook 端也需要幂等性吗?
是的。同一个 webhook 请求可能会多次到达,因为系统会不断重试,直到你的端点确认为止。webhook文档 (在新标签页中打开)中提到要进行去重。X-Myaza-Delivery标题,或在标题上verificationId记录已处理的内容,并在第二次执行时将处理程序设为空操作。幂等发送和幂等接收相结合,才能确保从客户的角度来看,流程只执行一次。
检查清单
一
requestId根据逻辑验证,源自您自己的记录并已命名空间。第一次尝试之前已存储;重试时会读取,不会重新生成。
穿过 SDK 的
metadata当 SDK 为您提交。重试
429,502,503,504仅网络故障,采用退避机制RateLimit-Reset价值。例如这样的结果
declined由……处理reasonCode绝不能通过重新提交的方式。Webhook 处理程序已去重
X-Myaza-Delivery或者verificationId。
完整的请求合约位于创建验证参考文档 (在新标签页中打开)中,开发者中心链接了其余部分。 API 。
参考来源



