向 Intuit/QuickBooks Payments API 提交卡 token 时出错
结论
PMT-4000: token is invalid 中的 token 是支付卡 token,不是 OAuth access token。
支付卡 token 是否有效,不能只看字符串格式。它与生成时使用的以下上下文绑定:
- QuickBooks Payments 环境;
- Intuit 应用;
- App Token;
- 对应的 Payments/merchant 上下文。
所以,即使 token 是格式正确的 UUID,只要生成 token 和提交 charge 时使用的环境或应用凭据不同,Payments API 仍会将其判定为无效。
这个问题分为两部分:
- 旧版 sandbox JavaScript 文件会把 card data 发送到 Intuit 内部的 E2E 环境。该环境生成的 token 无法用于公开 sandbox。
- 换成正确的 sandbox tokens endpoint 后,tokenization 请求还必须使用与服务器端 OAuth 2.0 凭据同属一个应用、同一环境的 App Token。只更换 URL 仍然不够。
OAuth token 与支付卡 token 是两回事
两类 token 的用途不同:
- OAuth 2.0 access token 用于认证服务器端发出的 Payments API 请求。
- Card token 是 card data tokenization 生成的一次性支付凭据,需要写入 charge 请求的
token字段。
OAuth access token 出错时,通常会返回 AuthenticationFailed 或 HTTP 401。当前响应是:
{
"code": "PMT-4000",
"message": "token is invalid.",
"detail": "token"
}
这说明请求已经通过认证并进入支付参数校验阶段,出错的是 charge payload 中的 card token。
为什么旧 JavaScript 生成的 token 无效
旧脚本如下:
<script src="https://js.appcenter.intuit.com/Content/IA/intuit.ipp.payments.sandbox-0.0.3.js"></script>
它实际使用的 tokenization 地址是:
https://transaction-api-e2e.payments.intuit.net/v2/tokens
这是 Intuit 的内部 E2E 测试环境,不是公开 sandbox。该环境生成的 token 不能提交到:
https://sandbox.api.intuit.com/quickbooks/v4/payments/charges
正确的 sandbox tokenization endpoint 是:
https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens
路径中必须包含 /quickbooks。https://sandbox.api.intuit.com/v4/payments/tokens 是另一个地址。
为什么改用正确 endpoint 后仍会失败
直接请求正确的 endpoint 时,还需要携带正确的 App Token。下面的请求只更换了 URL,没有提供 App Token:
jQuery.ajax({
url: "https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens",
type: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(cardData)
});
tokenization endpoint 可能会返回一个外观正常的 token,但这个 token 不一定属于服务器提交 charge 时使用的应用上下文。
以下凭据必须全部来自同一个 Intuit 应用的 sandbox 配置:
- 浏览器 tokenization 使用的 App Token;
- 服务器端使用的 OAuth 2.0 Client ID;
- 与该 Client ID 对应的 OAuth access token;
- access token 连接的 sandbox company/merchant;
- sandbox Payments API endpoint。
以下配置不能混用:
- production App Token 与 sandbox OAuth access token;
- 应用 A 的 App Token 与应用 B 的 OAuth access token;
- E2E tokenization 与公开 sandbox charges;
- sandbox tokenization 与 production charges。
正确的 tokenization 请求
按照这个接口版本的认证方式,浏览器端需要使用 sandbox App Token 调用 tokens endpoint。旧版接口通常通过 Basic Authentication 传递 App Token,并将密码留空:
const appToken = "YOUR_SANDBOX_APP_TOKEN";
const cardData = {
card: {
number: $("#tokenize_cc-number").val(),
expMonth: $("#tokenize_cc-expmonth").val(),
expYear: $("#tokenize_cc-expyear").val(),
cvc: $("#tokenize_cc-cvc").val(),
address: {
streetAddress: $("#tokenize_cc-address-street").val(),
city: $("#tokenize_cc-address-city").val(),
region: $("#tokenize_cc-address-region").val(),
country: $("#tokenize_cc-address-country").val(),
postalCode: $("#tokenize_cc-address-postalcode").val()
}
}
};
jQuery.ajax({
url: "https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens",
method: "POST",
contentType: "application/json",
dataType: "json",
headers: {
Authorization: "Basic " + btoa(appToken + ":")
},
data: JSON.stringify(cardData)
}).done(function (response) {
const cardToken = response.value;
if (!cardToken) {
console.error("Tokenization response did not contain a token", response);
return;
}
$("#card-token").val(cardToken);
$("#payment-form")[0].submit();
}).fail(function (xhr) {
console.error("Tokenization failed", xhr.status, xhr.responseJSON);
});
不同历史版本的响应结构可能有所差异。应根据实际响应确认 token 是否位于 response.value,不要把整个响应对象序列化后当作 charge token。
App Token 不是 OAuth client secret。不要把 OAuth client secret 或服务器端 bearer access token 放进浏览器代码。
服务器端提交 charge
服务器收到 card token 后,使用同一 sandbox 应用取得的 OAuth 2.0 access token 提交 charge:
curl --request POST \
'https://sandbox.api.intuit.com/quickbooks/v4/payments/charges' \
--header 'Authorization: Bearer YOUR_SANDBOX_OAUTH_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--header 'Request-Id: UNIQUE_REQUEST_ID' \
--data '{
"amount": "6992.83",
"currency": "USD",
"capture": true,
"token": "CARD_TOKEN_FROM_TOKENIZATION_RESPONSE",
"context": {
"mobile": false,
"isEcommerce": true
}
}'
每次业务操作都应生成唯一的 Request-Id。重试同一笔请求时是否复用该值,需要遵循 Payments API 的幂等规则。
排查清单
遇到同样的错误时,按以下顺序检查:
- tokenization 和 charge 是否都使用
sandbox.api.intuit.com。 - tokenization URL 是否包含
/quickbooks/v4/payments/tokens。 - 是否已经停用指向
transaction-api-e2e.payments.intuit.net的旧脚本。 - tokenization 请求是否携带 sandbox App Token。
- App Token 与 OAuth 2.0 Client ID 是否来自同一个 Intuit 应用。
- OAuth access token 是否由该 Client ID 签发,并连接到正确的 sandbox company。
- 应用是否已启用 QuickBooks Payments,access token 是否具备 Payments 所需权限。
- charge 中提交的是否为响应里的实际 token 值,而不是整个 JSON 对象。
- token 是否已被使用、已经过期,或生成后放置时间过长。
- 是否混用了 production、sandbox 或其他测试环境的配置。
OAuth 2.0 不代表 card tokenization 的数据格式会完全改变,主要变化在于服务器端 API 的认证方式。生成 card token 和消费该 token 的 charge 请求必须使用同一环境,并且属于同一个 Intuit 应用和 Payments 上下文。
备注:内容仅供参考。
