KYC 核验需求:NUBAN 收款账户信息核验切换至 Paystack
1. 背景与结论
当前 CRS 的 NUBAN 核验调用 Dojah GET /api/v1/kyc/nuban/status,将账户户名、KYC 状态、身份号码及姓名等结果永久缓存,并通过内部 POST /kyc/nuban/lookup 提供给 BNS。
本紧急需求将收款账户核验切换至 Paystack。经补充评估,推荐使用两个分层接口:GET /bank/resolve 完成同步账户解析和户名展示;POST /customer/{customer_code}/identification 提交 BVN + 收款账户的强归属核验,并以 webhook 异步确认结果。后者比单独 Resolve Account 更接近当前 Dojah 的 NUBAN KYC 场景。
上线结论:
- 可立即替换:收款账号 + 银行代码的有效性校验、户名回填和展示。
- 可替换强归属校验,但必须改为异步状态机:Paystack Customer Identification 先返回
202 / in progress,仅收到验签后的customeridentification.success才可标记 BVN 与账户强归属通过;failed或超时均不得放行。 - 不可将
GET /bank/resolve成功或POST /identification的 202 受理结果当作 BVN 一致性成功。 - 不以“免费”作为上线前提。Paystack 官方 API 文档未声明该接口免费;商务/财务须以 Paystack 商户 Dashboard、合同与实际 Live 账户额度/限流配置确认费用、支持银行和生产权限。
官方依据:Paystack Verification API — Resolve Account、Validate Customer;Customer API。
2. 目标与范围
目标
- 用户录入 10 位 NUBAN 和银行后,由服务端解析真实账户户名;客户端不接触 Paystack 密钥。
- 账户号或银行代码无效、账户无法解析、上游异常时,阻断本次收款账户提交/放款前校验,并给出可重试的业务提示。
- 明确区分“账户可解析”“姓名匹配”“BVN 强归属已核验”,不得将前两者标记为 BVN 已验证。
- 对外 BNS 调用尽量维持
POST /kyc/nuban/lookup契约,避免前端及非相关业务域直接改造。
本期范围
- CRS 的 NUBAN provider adapter、Paystack Customer 创建/映射、异步核验编排、webhook、错误映射、审计和监控。
- BNS 的收款账户户名回填、提交后“核验中”状态,以及 Paystack 成功回调后的准入控制。
- NUBAN 核验记录的 provider/能力等级/有效期隔离。
非本期范围
- BVN、NIN、活体、人脸等 Dojah 能力切换。
- 使用 Paystack
POST /bank/validate代替 BVN 核验。其官方示例为country_code=ZA,Nigeria/BVN 支持与合规口径未确认,不得直接纳入本期主链路。 - 将 Paystack 密钥、原始供应商响应或完整 NUBAN 返回客户端/日志。
3. 当前链路与影响面
flowchart LR App[App/H5] --> BNS BNS -->|POST /kyc/nuban/lookup| CRS CRS -->|同步解析| Paystack[Paystack] CRS -->|POST identification| Paystack Paystack -->|customeridentification webhook| CRS CRS --> BNS
| 层级 | 当前实现 | 本期改动 |
|---|---|---|
| CRS 客户端 | DojahApiClient.lookupNubanStatus 调 Dojah | 新增 PaystackApiClient.resolveAccount,使用 Authorization: Bearer {secret} |
| CRS 编排 | KycBusiness.lookupNuban、Dojah DTO、永久成功缓存 | 映射 Paystack 返回;按 provider 与能力等级缓存;设置有限有效期 |
| CRS 表 | t_kyc_nuban_status_record 存 identity/KYC 字段 | 新增 provider、verification_level、provider_code、provider_message;旧 Dojah 快照不可作为 Paystack 快照复用 |
| BNS 收款户名查询 | BankBusiness.verifyBankAccount 读取 accountName | 可保持调用方式,增加空户名与结果等级校验 |
| BNS BVN-账户一致性 | BvnBusiness 用 identityNumber 与输入 BVN 比对 | 必须改成发起异步核验并等待 webhook 成功;Paystack Resolve 无 identityNumber,不可沿用原判断 |
| BNS 进件补全 | ApplyBusiness 尝试回填 bankAcctName | 继续回填;失败不应绕过正式收款账户提交校验 |
4. 目标接口与数据契约
4.1 CRS 调用 Paystack
GET https://api.paystack.co/bank/resolve?account_number={NUBAN}&bank_code={BANK_CODE}
Authorization: Bearer {PAYSTACK_SECRET_KEY}account_number:仅接受 10 位数字 NUBAN。bank_code:使用 Paystack 银行字典的 code;不得默认复用 Dojah/内部银行编码。上线前需建立并验收内部bank_id -> paystack_bank_code映射。- 成功最小条件:HTTP 成功、
status=true、返回data.account_number与请求一致、data.account_name非空。 - 不记录完整 Authorization、完整 NUBAN、完整户名或上游原文到应用日志;审计记录使用脱敏值、哈希、trace_id 和 provider request id(若有)。
4.2 建议内部响应扩展
保留现有 accountName、accountNumber、bank 字段的兼容读取,同时新增以下字段;identityNumber 等 Dojah 专属字段在 Paystack 结果中必须为 null,不得伪造。
| 字段 | 说明 |
|---|---|
provider | 固定为 PAYSTACK |
verificationLevel | ACCOUNT_RESOLVED / NAME_MATCHED / STRONG_OWNERSHIP_VERIFIED |
accountResolved | 是否满足 4.1 成功最小条件 |
nameMatchResult | MATCH / MISMATCH / NOT_AVAILABLE |
verifiedAt、expireAt | 本次结果与重新核验时间 |
reasonCode | 对外稳定业务码;不直接透传供应商文本 |
4.3 强归属校验:Customer Identification
用户给出的
curl https://api.paystack.co/customer/{customer_code}/identification缺少 method 和 body;该接口是 POST,不是 GET。
POST https://api.paystack.co/customer/{customer_code}/identification
Authorization: Bearer {PAYSTACK_SECRET_KEY}
Content-Type: application/json
{
"country": "NG",
"type": "bank_account",
"account_number": "{NUBAN}",
"bvn": "{BVN}",
"bank_code": "{PAYSTACK_BANK_CODE}",
"first_name": "{BVN_KYC_FIRST_NAME}",
"last_name": "{BVN_KYC_LAST_NAME}",
"middle_name": "{BVN_KYC_MIDDLE_NAME}"
}customer_code不是 BNS 的cust_id,必须是同一 Paystack 商户/密钥下已创建 Customer 返回的CUS_*值。当前 BNSbns_cust有 email/姓名/手机号字段,但没有 Paystack Customer 映射;现有 IMS 仅在绑卡交易结果中保存customer_code,不能作为 KYC 主链路的可靠来源。- 先由 CRS 为业务客户
POST /customer创建(或复用自建映射表中同一 merchant 的 Customer),持久化cust_id + paystack_merchant + customer_code;email 为空或未经确认时须先补齐合规可用的唯一 email,不能依赖绑卡时临时生成的 email。 - 接口成功只表示已受理(官方示例为
202和Customer Identification in progress)。核验结论来自验签 webhook:customeridentification.success写入STRONG_OWNERSHIP_VERIFIED;customeridentification.failed写入失败原因(官方原因包括账户无法解析、账户名或 BVN 错误、账户号或 BVN 错误)。 - success webhook 仅回传脱敏 BVN/NUBAN 等识别信息;户名展示仍以
/bank/resolve返回为准,或成功后以GET /customer/{customer_code}读取 Paystack 已更新的客户姓名。两者均须与本地请求指纹(客户、BVN、银行、NUBAN、规则版本)严格对应。 - 新建 CRS webhook handler,验
X-Paystack-Signature(HMAC-SHA512 原始请求体),按事件 ID/请求指纹幂等处理。现有 PMT/IMS 的验签代码只处理charge.success等支付事件,不能直接承担 KYC 事件路由。
5. 业务规则
- **账户可解析:**Paystack 成功且回传账号一致、户名非空,标记
ACCOUNT_RESOLVED;允许回填/展示脱敏户名。 - **姓名匹配:**在等待异步强核验时,以已完成 BVN KYC 的法定姓名与 Paystack
account_name做服务端标准化匹配(大小写、首尾空格、连续空格、标点;规则版本需留痕)。命中标记NAME_MATCHED,但不能放宽强归属场景。 - **BVN 强归属:**仅收到验签的
customeridentification.success,且回调与本地 pending 请求的客户、银行、NUBAN、BVN 指纹匹配时,产生STRONG_OWNERSHIP_VERIFIED;Paystack Resolve 或 Identification 受理结果永远不得产生该等级。 - 放款/绑卡准入:
- 仅要求收款账户可用的场景:最低
ACCOUNT_RESOLVED。 - 规则要求“本人账户”的场景:最低
NAME_MATCHED;风控/合规若要求 BVN 强归属,则本需求上线前必须提供独立强核验通道,否则该场景不切换。
- 仅要求收款账户可用的场景:最低
- 银行、账号、BVN KYC 姓名任一变化,旧结果立即失效并重新解析。
- Paystack 调用超时、5xx、限流、网络异常或 webhook 超时均为“未知”,不等同于账号不存在;允许用户稍后重试,严禁自动伪成功或静默降级。重复提交前必须按请求指纹查询 pending 状态,避免重复创建 Customer 或重复提交核验。
- 不做 Dojah→Paystack 静默回退。若紧急期需要双跑,仅允许灰度观测时同请求并行比对,用户决策仍以被选主 provider 的结果为准,且不得因任一方异常放行。
6. 数据、缓存与迁移
- 成功记录不再永久有效;建议
expire_at = verified_at + 24h(最终 TTL 由风控/支付确认)。放款、签约和提现/收款账户变更前必须复验。 - 缓存/唯一查询键至少包含
provider + bank_code + account_number + verification_rule_version,避免读到 Dojah 历史成功记录而跳过 Paystack。 - 既有 Dojah 历史记录只作审计,不得被标记为
PAYSTACK或覆盖其 provider。新增字段先允许空,回填 provider 为DOJAH,再启用 Paystack 主链路。 - 原始响应如需留存,仅加密存储、最小权限访问和明确保留期;业务日志只存脱敏账号(如
******8151)与户名摘要。
7. 异常码与前端交互
| 场景 | 内部码(建议) | 用户提示 | 处理 |
|---|---|---|---|
| 参数/格式错误 | BANK_ACCOUNT_INVALID_INPUT | 请检查银行和 10 位账户号 | 不调用三方 |
| 内部银行未映射 Paystack code | BANK_CODE_NOT_SUPPORTED | 当前银行暂不支持,请更换银行或稍后重试 | 告警并补字典 |
| 上游明确无法解析 | BANK_ACCOUNT_NOT_RESOLVED | 未能核验该收款账户,请核对后重试 | 不允许提交 |
| 上游超时/5xx/限流 | BANK_ACCOUNT_VERIFY_UNAVAILABLE | 账户核验暂不可用,请稍后重试 | 不允许提交、可重试 |
| 姓名不匹配 | BANK_ACCOUNT_NAME_MISMATCH | 收款账户户名与认证信息不一致 | 不允许本人账户场景提交;进入人工复核策略(如启用) |
| 强归属能力缺失 | BANK_ACCOUNT_OWNERSHIP_UNVERIFIED | 当前无法完成账户归属核验 | 强归属场景阻断,不伪造成功 |
前端沿用“选择银行 + 输入 NUBAN → 服务端核验 → 展示脱敏户名/结果”的交互;不得直接请求 Paystack,不展示供应商名、密钥或原始错误。
8. 发布、监控与回滚
- **上线前门禁:**确认 Live secret key、费用/额度、生产 IP/权限、Paystack 支持的 Nigeria 银行清单及内部银行码映射;使用已获授权的脱敏样本完成成功、账户不存在、错银行码、超时和限流联调。
- **灰度:**按配置
nuban.provider=DOJAH|PAYSTACK和灰度比例启用;首日保留 Dojah 只读审计能力,不改写历史结果。严禁将两家结果混存为同一快照。 - **监控:**按 provider、银行、接口状态、错误码统计请求量、成功率、P95/P99、超时率、
NAME_MATCHED率、内部银行码未映射数、缓存命中率;不输出明文 PII。 - **告警建议:**5 分钟请求失败率/超时率、任一银行异常集中、未映射银行码、成功率相对灰度基线显著下降均触发 P1/P2 告警。
- **回滚:**关闭 Paystack 灰度并切回
DOJAH配置;不删除 Paystack 审计记录。若本次已将强归属场景降为姓名匹配,回滚须同时恢复对应准入策略,防止标准漂移。
9. 验收标准
- AC-01:输入有效 NUBAN + 已映射银行,CRS 使用 Paystack
/bank/resolve,返回账号一致且户名非空时,BNS 能得到户名和provider=PAYSTACK、verificationLevel=ACCOUNT_RESOLVED。 - AC-02:
/bank/resolve成功或/identification返回 202 时,系统均不产生强归属成功;只有验签且指纹匹配的customeridentification.success才产生强归属成功。 - AC-03:需要本人账户的流程只在姓名匹配规则命中后通过;不匹配时返回稳定业务码并阻断。
- AC-04:错银行码、账号不存在、4xx、超时、5xx、限流分别映射为可辨别的内部业务码;上游异常绝不被当作“账号不存在”或成功。
- AC-05:银行映射缺失时不调用 Paystack,记录告警并返回
BANK_CODE_NOT_SUPPORTED。 - AC-06:更换银行/账号、KYC 姓名变化或结果过期后会重新核验;旧 Dojah 永久快照不会命中 Paystack 缓存。
- AC-07:日志、监控、DB 审计中无 Secret Key、完整 NUBAN、完整户名或完整上游响应;具备 trace_id 追踪。
- AC-08:Customer 不存在时按同一 merchant 创建并保存
customer_code映射;重复请求不重复创建/提交,webhook 重放不重复改变业务状态。 - AC-09:灰度关闭可恢复 Dojah 主链路,且不会修改 BVN/NIN/活体/人脸的 Dojah 调用。
10. 待确认的决策(阻塞强归属场景上线)
- 强归属场景是否以 Paystack
customeridentification.success作为最终准入证据;若不接受,必须保留/补充独立核验供应商。 NAME_MATCHED的姓名规范化规则、允许差异范围和人工复核策略由风控/合规确认。- Paystack Live 账户对 Nigeria
GET /bank/resolve的费用、日限额、QPS、可用银行和 SLA,以商户侧书面确认及实测为准。 - 24 小时建议缓存有效期是否满足资金风控;若不满足,确认更短 TTL 及放款前强制复验时点。