对标行业卡台(如 GEO)的 Webhook 通知 能力:卡片操作结果、卡交易结果、3DS 验证码、共享池变更等异步事件,由平台 HTTP POST 推送到你在控制台配置的回调地址。
与上游 GEO 的差异:上游对商户侧多使用 type + 加密 data 包;本平台对下游商户采用 明文 JSON + RSA 请求头签名(更易接入)。事件语义一一对应,见下文「与上游 type 对照」。
| 项 | 说明 |
|---|
| 请求方式 | POST |
| Content-Type | application/json |
| 回调地址 | 商户自己的公网 URL(控制台 → 开发者 → Webhook,建议 HTTPS)。不是 openapi.cafinx.com / uat-openapi.cafinx.com |
| 成功响应 | 尽快返回 HTTP 2xx 表示已接收 |
| 失败/超时 | 平台延迟多次补发(持久化退避重试,直至 SUCCESS / DEAD) |
| 幂等 | 用 body 内 eventId 去重(同一事件可能因重试多次到达) |
| 安全 | 请求头 X-Cafinx-Signature:平台对 原始 body 字节 做 RSA-SHA256 签名 |
- 填写公网可达 URL(生产建议 HTTPS)
- 启用开关
- (可选)
eventTypes:订阅列表,逗号分隔;支持 card.*、transaction.*、card_group.*;空或 * = 全部
平台在保存与投递前做 SSRF 校验(拒绝内网 / 回环 / 云元数据地址)。
| 请求头 | 必填 | 说明 |
|---|
X-Cafinx-Event | M | 事件类型,同 body.eventType,如 transaction.authorized、card.issued |
X-Cafinx-Timestamp | M | 毫秒时间戳(与 body.timestamp 一致) |
X-Cafinx-Signature | M | Base64( RSA-SHA256( 原始 body, 平台 App 私钥 ) ) |
| 字段 | 类型 | 必填 | 说明 |
|---|
eventId | string | M | 平台唯一事件 ID,幂等键 |
eventType | string | M | 业务类型(见下表) |
timestamp | long | M | 毫秒时间戳 |
userNo | string | M | 商户号(如 CFX-XXXX,对标上游 userNo) |
data | object | M | 业务数据(随事件不同,见各节) |
{
"eventId": "evt_8f3a2c1b0d9e",
"eventType": "transaction.authorized",
"timestamp": 1710000000000,
"userNo": "CFX-XXXX",
"data": { }
}
上游 GEO type | 本平台 eventType | 说明 |
|---|
card_transaction | transaction.authorized / declined / refunded / reversed / settled | 卡消费/授权/退款等交易结果 |
card_operate | card.issued / card.recharge.succeeded / card.withdraw.succeeded / card.frozen / card.unfrozen / card.closed / card_group.* | 开卡、充值、转出、冻解、销卡、共享池等订单/操作结果 |
card_3ds_otp | card.3ds.otp | 3DS 验证码 |
开卡/充值等同步接口失败直接返回业务错误码,不会再推送 *.failed Webhook(请按 API 响应处理)。
消费授权、拒绝、退款、冲正、清算等,上游推送给平台后转发为下列事件。
| eventType | 说明 |
|---|
transaction.authorized | 授权/消费成功 |
transaction.declined | 授权拒绝/失败 |
transaction.refunded | 退款入账成功 |
transaction.reversed | 冲正成功 |
transaction.settled | 清算/差额结算类成功 |
| 字段 | 类型 | 必填 | 说明 | 对标 GEO |
|---|
txId | string | M | 本地交易记录 ID | — |
transactionNo | string | M | 交易流水号 | recordNo |
cardId | string | M | 本地卡 ID | cardId |
cardNoLast4 | string | O | 卡号后四位 | cardNo 掩码一部分 |
cardMask | string | O | 掩码卡号 | card 展示 |
type | string | M | 交易类型:AUTH / PURCHASE / REFUND / REVERSAL 等 | transType 映射 |
transType | string | O | 上游原始交易类型码 | transType |
amount | string | M | 交易金额(十进制字符串) | localCurrencyAmt / transCurrencyAmt |
currency | string | M | 币种,如 USD | localCurrency / transCurrency |
merchantName | string | O | 商户名称 | merchantName |
mcc | string | O | 商户 MCC | merchantCategoryCode |
status | string | M | APPROVED / DECLINED / SETTLED 等 | transStatus |
authCode | string | O | 授权码 | approvalCode |
declineReason | string | O | 拒绝原因(declined 时) | respCodeDesc |
fee | string | O | 手续费 | fee |
loadedBalance | string | O | 交易后卡余额 | — |
{
"eventId": "evt_tx_001",
"eventType": "transaction.authorized",
"timestamp": 1710000000000,
"userNo": "CFX-10086",
"data": {
"txId": "900311",
"transactionNo": "TX20260701123456001",
"cardId": "1001",
"cardNoLast4": "6641",
"cardMask": "**** **** **** 6641",
"type": "AUTH",
"transType": "AUTH",
"amount": "12.50",
"currency": "USD",
"merchantName": "AMAZON.COM",
"mcc": "5942",
"status": "APPROVED",
"authCode": "A1B2C3",
"declineReason": null,
"fee": "0",
"loadedBalance": "487.50"
}
}
开卡、充值、转出、冻解、销卡及共享池操作成功后异步通知(对标上游 transactionType 15/16/19/18/26 等)。
| eventType | 说明 | 对标 GEO transactionType(参考) |
|---|
card.issued | 开卡成功 | 15 办卡 |
card.recharge.succeeded | 卡充值成功 | 16 充值 |
card.withdraw.succeeded | 卡转出成功 | 18 退款/转出 |
card.closed | 销卡成功 | 19 销卡 |
card.frozen | 冻结 | 卡片状态变更 |
card.unfrozen | 解冻 | 卡片状态变更 |
card_group.created | 共享池创建 | 26 共享卡组申请 |
card_group.recharge.succeeded | 共享池充值 | 池充值 |
card_group.closed | 共享池注销 | 池关闭 |
| 字段 | 类型 | 说明 |
|---|
cardId | string | 本地卡 ID |
cardNoLast4 | string | 后四位 |
bin | string | 卡头 |
alias | string | 别名 |
openFee | string | 开卡费 |
topup | string | 首充金额 |
totalCharge | string | 本次总扣款 |
externalCardId | string | 上游卡 ID |
providerCode | string | 上游通道 |
| 字段 | 类型 | 说明 |
|---|
cardId | string | 卡 ID |
amount | string | 入卡金额 |
fee | string | 手续费 |
feeRate | string | 费率 |
totalDebit | string | 钱包总扣款 amount+fee |
loadedBalance | string | 充值后卡余额 |
walletBalance | string | 扣款后钱包余额 |
| 字段 | 类型 | 说明 |
|---|
cardId | string | 卡 ID |
cardNoLast4 | string | 后四位 |
amount | string | 转出金额 |
loadedBalance | string | 转出后卡余额 |
walletBalance | string | 入账后钱包余额 |
| 字段 | 类型 | 说明 |
|---|
cardId | string | 卡 ID |
cardNoLast4 | string | 后四位 |
reason | string | 原因 |
source | string | GATEWAY / PLATFORM |
status | string | FROZEN / ACTIVE |
| 字段 | 类型 | 说明 |
|---|
cardId | string | 卡 ID |
cardNoLast4 | string | 后四位 |
returnedAmount | string | 退回钱包的卡余额 |
status | string | CLOSED |
card_group.created
| 字段 | 说明 |
|---|
groupId | 本地池 ID |
externalGroupId | 上游池 ID |
groupName | 名称 |
balance | 池余额 |
totalAuthMoney | 累计授权额度 |
totalDebit | 建池时钱包扣款 |
card_group.recharge.succeeded
| 字段 | 说明 |
|---|
groupId / amount / fee / feeRate / totalDebit | 入池与扣款 |
balance / totalAuthMoney / walletBalance | 余额快照 |
card_group.closed
| 字段 | 说明 |
|---|
groupId | 池 ID |
status | 2=已注销 |
refundedAmount | 退回钱包金额 |
{
"eventId": "evt_card_issued_01",
"eventType": "card.issued",
"timestamp": 1710000000000,
"userNo": "CFX-10086",
"data": {
"cardId": "1001",
"cardNoLast4": "6641",
"bin": " rec553412",
"alias": "ads-01",
"openFee": "1.00",
"topup": "50.00",
"totalCharge": "51.00",
"externalCardId": "geo_card_xxx",
"providerCode": "GEO"
}
}
| eventType | 说明 |
|---|
card.3ds.otp | 3DS 验证码(敏感,短时有效;文档旧名 card.3ds.challenge 同义) |
| 字段 | 类型 | 必填 | 说明 | 对标 GEO |
|---|
otp | string | M | 验证码 | otp |
cardId | string | M | 本地卡 ID | cardId |
externalCardId | string | O | 上游卡 ID | — |
cardMask / cardNo 语义 | string | M | 掩码卡号 | cardNo |
cardNoLast4 | string | O | 后四位 | — |
merchantName | string | M | 交易商户名 | merchantName |
transactionAmount | string | O | 交易金额 | transactionAmount |
transactionCurrency | string | O | 交易币种 | transactionCurrency |
otpId | string | O | 本地 OTP 记录 ID | — |
expireAt | string | O | 过期时间 | — |
ttlMinutes | number | O | 有效分钟数(约 5) | — |
安全:otp 勿长期明文落库;用完即弃。
{
"eventId": "evt_3ds_01",
"eventType": "card.3ds.otp",
"timestamp": 1710000000000,
"userNo": "CFX-10086",
"data": {
"otpId": "88",
"cardId": "1001",
"externalCardId": "geo_card_xxx",
"cardMask": "**** **** **** 6641",
"cardNoLast4": "6641",
"otp": "123456",
"merchantName": "NETFLIX.COM",
"transactionAmount": "15.99",
"transactionCurrency": "USD",
"expireAt": "2026-07-28T15:30:00",
"ttlMinutes": 5
}
}
| eventType | 说明 | data 要点 |
|---|
recharge.succeeded | 链上钱包充值到账 | orderId, asset, amount, txHash? |
平台用平台 App 私钥对 body 原文签名。你用平台公钥验签:
- 公钥:控制台,或登录后
GET /v1/developer/app-public-key
- 验签对象是收到的原始请求体字节,不要 JSON 反序列化后再 stringify
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureBase64, platformPublicKeyPem) {
return crypto.verify(
'RSA-SHA256',
Buffer.from(rawBody, 'utf8'),
platformPublicKeyPem,
Buffer.from(signatureBase64, 'base64'),
);
}
Express 请用 express.raw({ type: '*/*' }) 拿原始 body。
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
public static boolean verifyWebhook(String rawBody, String signatureB64, PublicKey appPublicKey)
throws Exception {
Signature s = Signature.getInstance("SHA256withRSA");
s.initVerify(appPublicKey);
s.update(rawBody.getBytes(StandardCharsets.UTF_8));
return s.verify(Base64.getDecoder().decode(signatureB64));
}
| 商户响应 | 平台行为 |
|---|
| HTTP 2xx | 标记 SUCCESS,不再重试 |
| 非 2xx / 超时 / 网络失败 | 退避重试:PENDING → FAILED → 多次后 DEAD |
未知 eventType | 也请返回 2xx,避免无意义死循环 |
- 控制台可查看投递记录,并对 FAILED/DEAD 手动重发
- 未配置 URL 或未启用时:业务仍成功,Webhook 静默跳过
- 先落库 / 入队再回 2xx,重活异步处理
- 校验时间戳窗口,拒绝过期回放
card.3ds.otp 的 otp 勿长期明文落库
- 生产仅使用 HTTPS 回调地址
- 至少订阅:
card.*、transaction.*、card.3ds.otp、card_group.*
| 分类 | eventType | 状态 |
|---|
| 交易 | transaction.authorized / declined / refunded / reversed / settled | 已上线 |
| 卡片操作 | card.issued / recharge.succeeded / withdraw.succeeded / frozen / unfrozen / closed | 已上线 |
| 3DS | card.3ds.otp | 已上线 |
| 共享池 | card_group.created / recharge.succeeded / closed | 已上线 |
| 钱包 | recharge.succeeded | 已上线 |
| 同步失败 | (无 Webhook) | 看 API 错误码 |
参考上游文档结构:GEO Webhook 通知。