四条状态轴各自独立推进 —— 申请单、卡片、物流、补件。这一页是它们的全集。
状态机与取值
发卡这条线上有四条状态轴,任何一条都不由另一条派生:
| 轴 | 挂在哪 | 由谁推进 |
|---|---|---|
| 申请单 | GET /v1/cards/applications/{id} 的 status | 我方执行器 + 上游 |
| 卡片 | GET /v1/cards/{id} 的 status | 会员操作 + 风控 + 上游 |
| 物流 | GET /v1/cards/applications/{id}/shipment 的 status | 我方运营录入 |
| 补件 | 申请单详情里的 supplement.status | 上游要求 + 会员提交 |
⚠ 别按「物流已签收」推断「卡能刷了」。 那是两条轴:包裹到手之后
还要绑卡、还要激活。同理,申请单到了终态也不代表卡是
active——实体卡的申请单终态之后卡还是
unactivated。
一、申请单状态
下单那一刻只有三档
POST /v1/cards/applications 的响应是裁剪过的,只会是:
| 值 | 含义 |
|---|---|
submitted | 已受理,我方在推进 |
pending_review | 该产品配了人工审核,等我方审 |
pending | 已受理,排队中 |
之后回查会看到内部全集
GET /v1/cards/applications/{id} 返回的是内部状态原文,没有裁剪。下面这 20 档你都可能看到:
在途(15 档)
| 值 | 含义 | 谁在动 |
|---|---|---|
pending_merchant_funds | 你的预付余额不足,这张单在排队 | 等你充预付 |
awaiting_payment | 草稿,还没扣费 | 30 分钟不付自动取消 |
submitted | 已提交 | 我方 |
pending_review | 等人工审核 | 我方 |
accepted | 已受理,等上游确认 | 上游 |
processing | 上游处理中 | 上游 |
need_docs | 上游要求补充用卡人材料 | 等会员 |
docs_submitted | 材料已交,等上游复核 | 上游 |
allocating | 实体卡:从库存预占一张 | 我方 |
picking | 实体卡:待发货 | 我方运营 |
shipped | 实体卡:已寄出 | 等会员收 |
awaiting_bind | 会员点了「已收到」,等绑卡 | 等会员 |
binding | 绑卡校验中 | 我方 + 上游 |
pending_activation | 已绑卡,等激活 | 等会员 |
refunding | 取消流程在退费 | 我方 |
终态(5 档) —— 到了这里就不会再变:
| 值 | 含义 |
|---|---|
issued | 开出来了(虚拟卡的成功终点) |
failed | 失败,看 failure_code |
rejected | 被拒(人工审核或上游) |
refunded | 已退费 |
canceled | 已取消 |
⚠
pending(下单响应)与pending_merchant_funds(回查)是同一件事的两个名字。前者是裁剪后的,后者是原文。你的状态映射表必须同时认得两个,
否则一次「预付不足排队」会在你的系统里变成一个未知状态。
已记入已知问题:这两处不一致,
我方会收敛到裁剪那一侧。现在就把
pending_merchant_funds当pending处理,到时候你一行不用改。
⚠
pending_activation是实体卡申请单的事实终点,虽然它在「在途」那一栏。没有任何代码会把它推走 —— 激活改的是卡片那条轴。
别写「等申请单变成
issued再放行」的逻辑,实体卡永远等不到。
别把在途/终态硬编码成清单
判断该不该继续轮询,用「是不是那 5 个终态之一」,不要列举在途档。我方加一档在途状态是加一行代码,不通知;而你那份在途清单漏了它的表现是这张单从你的列表里静默消失 —— 用户以为申请没了。
二、卡片状态
┌──────────── 会员自助 ────────────┐
pending → unactivated → active ⇄ frozen
│ ↑ ↓
│ freezing / unfreezing(过渡态)
│
├─→ risk_frozen 风控冻结,**会员解不了**
├─→ suspended 运营暂停,**会员解不了**
├─→ lost → reissuing → replaced
├─→ expired
└─→ closing → closed
| 值 | 能刷 | 能充值 | 会员能自助解冻 |
|---|---|---|---|
pending | ✗ | ✓ | — |
unactivated | ✗ | ✗ | — |
active | ✓ | ✓ | — |
freezing | ✗ | ✗ | ✗(等它到 frozen) |
frozen | ✗ | ✓ | ✓ |
unfreezing | ✗ | ✗ | ✗(等它到 active) |
risk_frozen | ✗ | ✗ | ✗ 风控处置 |
suspended | ✗ | ✗ | ✗ 运营处置 |
lost / reissuing / replaced | ✗ | ✗ | — |
expired / closing / closed | ✗ | ✗ | — |
⚠
frozen能充值,过渡态不能。 这不是笔误:冻结是「先别刷」,不是「先别收钱」;而
freezing/unfreezing期间卡的状态还没定,往里推钱是往一张状态未定的卡里推钱。
⚠ 冻结与解冻是两个过渡态,不是一个「处理中」来回用。
合并成一个正是「解冻之后报卡状态无效 + 充值失败」那个线上故障的成因。
⚠
risk_frozen与suspended不要在界面上做成「解冻」按钮。会员点了必然失败。它们分别由风控与运营解除。
过渡态怎么处理:freezing / unfreezing / pending / closing / reissuing 这五档只能等。别在上面再发一条指令 —— 重复的冻结请求不会加速它。
三、物流状态
15 档。由我方运营录入,不是轮询能催出来的 ——包裹在路上时服务端那一侧不会自己前进,每 45 秒问一次纯属白打请求。
在途:draft · created · awaiting_pickup · in_transit · out_for_delivery · returning
异常(这几档要单独捞出来给运营看,混在在途列表里会永远排在后面没人管):address_issue · delivery_failed · stalled · refused · returning · lost · damaged
终态:received · returned · lost · damaged
还有一档 delivered(承运商标记已投递)—— 它不是终态,received 才是(会员确认收到)。
⚠ 退回件绝不自动回可用库存。
returned之后的处置走另一条流程,不在这条轴上。
四、补件
申请单进 need_docs 时,详情里的 supplement 从 null 变成一个对象:
"supplement": {
"status": "pending",
"required_items": ["cert_front", "address"],
"deadline": "2026-08-20T00:00:00.000Z",
"submit_url": ""
}
没有待办工单时 supplement 是 null,不是缺字段。
status | 含义 |
|---|---|
pending | 等会员交材料 —— 要催他 |
submitted | 已交,等上游复核 —— 催了也没用 |
required_items 的取值
cert_front · cert_back · address · name · birth · cert_id · other
⚠ 认不出上游给的原因时我方回落成
["other"],不会给空数组 ——一屏「需要补件」而下面什么都没有,用户根本不知道要交什么。
你也照这个姿态处理:认不出的项显示成「其它材料,请联系客服」。
deadline 是真的
过期后申请单转 failed 并退开卡费。 这与汇款补件刻意不给截止时间相反 —— 那边给 null,这边是库里的死值。
拿它画倒计时是安全的。空串 = 老数据没设过期时间。
怎么交:换一张托管屏链接
POST /v1/cards/applications/{id}/supplement-sessions
→ 201 { "hosted_url": "https://…/hosted/card-supplement/kyc_…", "expires_at": … }
把 hosted_url 交给终端用户。他在那一页重新拍摄并上传证件照,提交后你的申请单自动继续 —— 你不用再调任何接口。
详情里的 submit_via 恒为这个端点名,不是一条现成的 URL:链接是一次性的(绑会员 + 绑这张工单 + 24 小时),预先塞进详情等于把它变成常驻链接,而详情会被反复读、被日志记下来、被转发,那条 URL 能以那个会员的名义提交材料。要交材料时现调一次。
⚠ 只有
supplement.status是pending时才签得出票。已经交过(
submitted)时回 404 —— 再签一张等于给用户一个点进去只会看到「链接已失效」的入口。
为什么是托管屏而不是一个 JSON 提交口
这条线上「交材料」的实质是换掉证件影像 —— 我方递给上游的照片取自该会员此刻的实名档案,不取提交体里的任何字段。所以:
- 影像与 KYC 同一条边界,永不经你的服务器;
- 一个只收标量的提交口会把这一轮补件消耗掉、把同一份旧照片再推一次,用户那侧显示「已提交」,然后被同一个理由驳第二次 ——而这条路是一次性的(工单过期,申请单转
failed,开卡费退回,卡没开成)。
⚠ 待补项里的
name/birth/cert_id/address那几档,这一屏改不了(页面会如实标出来):改身份数据要重新过我方实名审核,
而换一张更清晰的照片不必 —— 后者若也退回重审,这个会员的转账 / 理财 /
汇款会因为实名等级不足而全线停摆,为的却是某一家发卡机构的图像质量意见。
这几档请引导用户联系你的客服。
失败原因(failure_code)
终态 failed / rejected 的详情里带 failure_code。它走对外码映射,不是内部码 —— 内部码携带机制细节,直通等于把内部状态机做成公开契约。
拿不到具体原因时它是空字符串,不是 null。