> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tokenization 与卡支付 FAQ

> 关于 Waffo 卡 Tokenization 最常见问题的解答：token 是什么、如何获取、token 生命周期、CIT 与 MIT、首次支付与后续扣款，以及如何处理过期或无效的 token。

本页回答商户关于绑卡和基于 token 支付最常问的问题。
字段级契约在
[Tokenization API 规范](/docs/api-reference/introduction)
和
[收单 API 规范](/docs/api-reference/introduction)。

## 什么是卡 Tokenization，`tokenId` 是什么？

Tokenization 用一个可复用的 `tokenId` 替代用户的原始卡信息。
在你这边安全地存储 `tokenId`；发起用户支付或
后端扣款时，用它代替卡信息。支持的
`tokenType` 是 `CARD`。一旦持有 token，你可以把它用于一次性
支付，以及你自己管理的订阅式扣款，按计划
或不按计划。

## 我如何绑卡并获取 token？

标准流程让原始卡数据不经过你的服务器：

1. 调用 `/api/v1/tokenization/generate`，带唯一的 `tokenRequestId`
   （幂等键）、你的 `merchantUserId`、`tokenType: "CARD"` 和一个
   `notifyUrl`。响应返回一个 `tokenSessionId`。
2. 在你的前端，初始化 Waffo JavaScript SDK
   （`@waffo/payment-sdk`）并调用 `tokenizationSubmit(tokenSessionId, tokenData)`。
   SDK 在把卡数据发送给 Waffo 之前会加密。卡的 `expiry`
   接受 `MM/YYYY`（输入时也接受 `MM/YY`），但查询和通知
   始终返回 `MM/YYYY`。
3. Waffo 向你的 `notifyUrl` 推送一条 `TOKENIZATION_NOTIFICATION` Webhook 通知，
   携带 `tokenId`、`tokenStatus` 和脱敏的卡信息
   （`maskedCardInfo`、`cardBin`、`cardScheme`、`cardType` 等）。
4. 随时使用 `/api/v1/tokenization/inquiry` 查询 token 详情——按
   `tokenId`（返回一条）或按 `merchantUserId`（可能返回多条）。

如果你的组织持有处理原始持卡人数据所需的 **PCI-DSS 认证**，
你可以改为在 `/api/v1/tokenization/generate` 的 `tokenData` 对象中直接传
`pan`、`name`、`expiry` 和 `cvv`；在这种
情况下 `tokenId` 会在 API 响应中同步返回。

## token 的生命周期是什么样的？

`tokenStatus` 有四个值：

| 状态 | 含义 |
| - | - |
| `UNVERIFIED` | 卡信息已提交，但尚未用该 token 成功支付过。它仍可作为 CIT 扣款。 |
| `VERIFIED` | 卡已验证。 |
| `EXPIRED` | token 已过期。 |
| `SUSPENDED` | 使用已被暂停。 |

每当 token 状态变化或 token 数据更新时，你会收到一条
`TOKENIZATION_NOTIFICATION`。处理后回复 `success`；如果 Waffo
收到 `failed` 或 `unknown`，Waffo 会在接下来一天内按退避
计划重试通知；计划参见
[Webhook 通知重试](/docs/zh/developer-docs/webhook/retry)。

## `tokenSessionId` 和 `tokenId` 各有效多久？

两个不同的时钟，其中只有一个是固定期限：

| 对象 | 有效期 |
| - | - |
| `/api/v1/tokenization/generate` 返回的 `tokenSessionId` | **短期有效**（自创建起一个固定的平台窗口）且一次性使用——一旦有卡针对它提交过，该会话即被消耗。在顾客即将输入卡时再打开它，不要提前 |
| 卡的 `tokenId` | 以下两者中**较早**的：自 token 创建起计算的一个固定多年平台有效期，或卡自身有效期月份的最后一刻 |

所以即将过期的卡产生的 token 会在其有效期月份结束时停止可用，
即使它是今天绑定的；而有效期很远的卡产生的 token
会跑完整个平台有效期。

两个实际后果：

* token 自身的过期时间戳**不**在
  `/api/v1/tokenization/inquiry` 的响应中。你拿回的是 `tokenStatus` 加上
  token 数据中的卡 `expiry`——如果你想在续订失败前提醒用户，
  这足以让你自己推导出上述规则。
* `tokenStatus` 由周期性扫描而不是在 token 失效的那一秒
  转为 `EXPIRED`，所以 token 可能在略过其计算出的过期时间后
  仍显示为可用。按过期规则安排；把状态和
  订单创建错误码当作最终定论。

## 同一个用户把同一张卡绑两次，我会得到两个 `tokenId` 吗？

不会。卡 token 在重新绑定时会去重：当以下全部
与一个未被移除的 token 匹配时，你会拿回**已有的**
`tokenId`——你的 `merchantId`（加上 `subMerchantId`，如果你传了）、相同的
`merchantUserId`、相同的卡、相同的持卡人姓名，以及相同的
`tokenType`。

值得围绕它设计的：

* 你不需要自己对已保存的卡列表去重；但要以 `tokenId` 作为你自己
  存储的键，因为第二次绑定返回的是第一个 token 而不是
  新的。
* `merchantUserId` 是键的一部分，所以**两个不同的 `merchantUserId`
  绑定同一张实体卡会得到两个不同的 `tokenId`**。没有
  跨用户的卡标识暴露给你用于关联。
* 同一张卡上不同的持卡人姓名被视为不同的
  凭据，会产生新的 token。
* `/api/v1/tokenization/remove` 之后，该 token 从查询中排除且
  永不复用——再次绑定该卡会发放一个新的 `tokenId`。

两个绑定调用都是幂等的，方式不同：

| 重复调用 | 行为 |
| - | - |
| 用已使用过的 `tokenRequestId` 调用 `/api/v1/tokenization/generate` | 返回已有会话的数据而不是打开新会话——超时后重试是安全的，也不会消耗第二个会话。 |
| 针对已经携带 token 的 `tokenSessionId` 提交卡 | 同一张卡时返回同样的结果。针对同一会话提交**不同的**卡会作为幂等冲突被拒绝——每张卡使用一个新会话。 |

## CIT 和 MIT 有什么区别，各在什么时候用？

| 模式 | 是什么 | 如何发送 |
| - | - | - |
| CIT（Customer Initiated Transaction） | 顾客主动参与支付——例如在收银台用已保存的卡支付。 | 调用 `/api/v1/order/create`，在 `userPaymentAccessToken` 中带上 token；不要设置 `merchantInitiatedMode`。 |
| MIT（Merchant Initiated Transaction） | 你在**没有顾客参与**的情况下向用户扣款——例如你自己安排的续订或按用量扣款。 | 调用 `/api/v1/order/create`，带 `userPaymentAccessToken` **并**把 `merchantInitiatedMode` 设为 `scheduled` 或 `unscheduled`。 |

两条 MIT 特有的规则需要注意：

* **CIT 可以用刚创建的 token**——首次支付就是验证卡的过程，
  所以你不需要等待"已验证"状态。
* **MIT 需要一个已经可用的存储凭据。** 如果 token 从未
  完成过成功支付（或其存储凭据已不再有效），
  MIT 扣款会以 `A0045` 被拒绝。实践中：首次扣款用
  CIT，之后的扣款用 MIT。
* `threeDsDecision` 字段（`3DS_FORCE` / `NO_3DS`）只对
  卡 / ApplePay / GooglePay 的 **CIT** 支付有效；对 MIT 会被忽略。

## 首次支付和后续扣款如何衔接？

1. 绑卡并获取 `tokenId`（见上文）。
2. 等待 token 到达可扣款状态——关注
   `TOKENIZATION_NOTIFICATION` 或轮询 `/api/v1/tokenization/inquiry`。
3. 对于用户参与的支付，用 `userPaymentAccessToken` 创建
   CIT 订单。
4. 对于之后你自己发起的扣款，用 `merchantInitiatedMode` 创建 MIT 订单
   （`scheduled` 用于你管理的固定节奏，
   `unscheduled` 用于事件驱动的扣款）。

## 为什么 `/order/create` 返回了 `AUTHORIZATION_REQUIRED` 而没有扣我已保存的 token？

因为针对已保存 token 的 CIT **不是**服务端扣款。当你发送
`userPaymentAccessToken` 而**不带** `merchantInitiatedMode` 时，Waffo 创建
订单并以 `orderStatus: AUTHORIZATION_REQUIRED` 和填充了的
`orderAction` 返回。此时还没有资金流动；持卡人仍有一步要完成。
这是一键 CIT 的正常结果，不是错误。

该怎么处理：

1. 解析 `orderAction` 并读取 `actionType`——`WEB` 表示把用户重定向到
   `webUrl`，`DEEPLINK` 表示重定向到 `deeplinkUrl`。
2. 在 Waffo 的托管收银台上，已保存的卡已被选中并以脱敏形式显示；
   **CVV** 通常是用户唯一需要填写的字段。之后可能跟随 3DS
   挑战。
3. 把 `AUTHORIZATION_REQUIRED` 视为非终态，等待支付
   通知（或轮询订单查询）获取结果。

绝不要仅基于"`/order/create` 是否返回成功"分支——要基于
`orderStatus` 分支，并且只要 `orderAction` 存在，就假设有一个用户步骤
尚未完成。

| | 使用已保存 token 的 CIT | MIT |
| - | - | - |
| 托管收银台 | 使用——用户从 `orderAction` 完成操作 | 从不；MIT 订单被刻意阻止进入收银台 |
| 创建响应 | 通常是 `AUTHORIZATION_REQUIRED` 加 `orderAction` | 扣款尝试在服务端运行；没有用户步骤，没有收银台 URL |
| 重新输入 CVV | 托管页面上通常会要求 | 不要求 |
| `threeDsDecision` | 对卡 / ApplePay / GooglePay 生效 | 忽略 |
| 失败后重试 | 在你的账户启用了用户重试的情况下，托管页面可以让用户在同一订单上再试一次 | 不在同一订单上重试——创建新订单再试 |

如果你需要在没有顾客在场的情况下扣款，那正是
`merchantInitiatedMode` 的用途；参见上文的 CIT/MIT 章节。

## token 过期了或支付被拒绝了，我该怎么办？

* 订单创建时的 `A0037`（用户支付 token 无效 / 已过期）或 `A0023`（因
  token 过期无法创建订单）：存储的 token 已
  不可用。调用 `/api/v1/tokenization/remove` 解绑它（按
  `tokenId`，或按 `merchantUserId` 解绑用户的所有 token），然后让用户
  绑定新卡。
* 绑定期间的 `B1002`（卡已过期）或 `B1001`（卡信息无效）：
  问题在卡本身——用户必须提供有效的卡。
* `B1005`（token 会话 ID 已过期）：`generate` 返回的 `tokenSessionId`
  已不再有效——再次调用 `/api/v1/tokenization/generate`。
* `B1003`（超出绑卡数量限制）和 `B1004`（当前商户合同不支持
  该卡组织）：这些取决于你签约的
  配置——请查看商户 Portal 或联系集成支持。

## 我在支付过程中绑卡。Tokenization 结果通知到哪里？

在商户 Portal（Settings → Integration）中针对你的 API key 配置一个专用的
**Tokenization 通知 URL**。Waffo 会把它快照到用该 key 创建的每个
订单上，绑卡结果会通知到那里。

* 它是一个独立通道：你的支付结果 `notifyUrl` 照常继续接收
  支付通知。
* 它**不是**订单创建参数——你不能按订单覆盖它。
* 如果未配置，Tokenization 通知行为不变。

## 我能设置用户的默认卡吗？

可以，通过一个专用端点而不是在订单创建时。调用
`/api/v1/saved-payment-methods/config`，带上 token 和你想要的操作：

| 操作 | 效果 |
| - | - |
| `SET_DEFAULT` | 把该卡设为用户的默认卡。每个用户最多一张默认卡，所以之前的默认卡会被替换。 |
| `SUSPEND` | 暂停该已保存的卡，使其不再被提供。 |

默认卡用于对顾客在收银台看到的已保存卡列表排序——
过期的卡被推到列表末尾。

## 用户的卡被银行换新了。存储的 token 还能用吗？

不要假设能用。Waffo 目前不会自行根据发卡行侧的更新
刷新存储的卡，所以请为重新绑定做好计划：

* 关注报告 token 不再可用的 Tokenization 和支付通知，
  并提示用户再次绑卡。
* 如果卡在钱包或发卡行侧被解绑，存储的凭据
  会变为不可用且无法重新激活。
* **过期的 token** 不能再扣款，但你仍可以调用
  `/api/v1/tokenization/remove` 解绑它，然后再向用户索要新的
  卡凭据。

对于订阅，在订阅上保留一个备用支付方式是
实际的保障——参见
[订阅生命周期](/docs/zh/support/subscription-lifecycle-management)。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.