商户开放平台 HTTP 接入规范

文档性质:对外通道说明 · 版本 API /open/v1 · 更新日期 2026-06-26
本文描述公网可调用的 URL、头字段与 JSON 契约,以及平台向商户推送的 Webhook 出站通知(含验签)。响应 data 中含 merchantCode,与请求头 X-Merchant-Code 一致。

2026-06-26 更新:新增会员自建卡组 创建 / 删除POSTDELETE /card-groups)及主钱包↔卡组子池 划款POST .../fund.../release);同一路径 /card-groups 以 HTTP 方法区分查询(GET)与创建(POST)。详见 §7.3–7.7
2026-06-23 更新:新增 §8 商户 Webhook 出站通知(回调签名、事件类型、验签示例);补充交易 Webhook txnType / status 码值表及 data 字段说明。
2026-06-18 更新:新增卡组只读/绑卡、消费约束目录、约束读取与用量投影;GET /cards 改为分页对象;开卡支持 platformCardGroupId;卡产品增加 spendingConstraintCeilingJson;多处 memberUserId 改为可选(省略时使用商户主会员)。

1. 基础约定与接入地址

说明
协议生产环境使用 HTTPS;字符集 UTF-8
数据格式请求与响应体为 application/json(GET 无 body 除外)。
基础路径所有开放接口均位于 {MERCHANT_API_BASE}/open/v1。将 {MERCHANT_API_BASE} 替换为服务方提供的网关根地址(不含末尾斜杠)。
示例https://api.example.com/open/v1/wallets
终端用户多数接口的 memberUserId 可选;省略时使用商户主会员账号(与 member-portal 主账号一致)。

2. 鉴权与请求签名

除「健康检查」接口外,所有 /open/v1/... 请求必须在 HTTP 头中携带访问凭据与签名。签名算法为 HMAC-SHA256,输出为小写十六进制字符串

2.1 待签名字符串(Canonical Request)

将以下 4 行按顺序用换行符 \n(ASCII 0x0A)连接,得到 UTF-8 字节序列后作为 HMAC 的输入消息:

  1. HTTP 方法:大写,如 GETPOST
  2. 路径:与网关可见的 Servlet 路径一致,例如 /open/v1/cards(不含域名与查询串)。
  3. 查询串:对查询参数名按字典序排序;同名参数多值按值排序;键值对形如 key=value,以 & 连接;键与值均需 URL 编码(空格为 %20)。若无查询参数,本行为空字符串(不要写 ?)。
  4. 请求体哈希:对请求体原始字节做 SHA-256,输出小写十六进制。无 body 时,对空字节序列做 SHA-256(注意与 {} 的字节不同)。
HMAC输入 = UTF8( method + "\n" + path + "\n" + canonicalQuery + "\n" + bodySha256Hex )
Signature  = 小写十六进制( HMAC_SHA256( UTF8(SecretKey), HMAC输入 ) )

SecretKey 为服务方在创建或轮换 API 凭据时向您下发的私钥;轮换宽限期内新旧密钥可能均可验签,以交付说明为准。

2.2 未开放路径

若请求的 URL 不在当前版本开放清单内,即使签名正确,网关也可能返回 HTTP 401 及业务错误码 40113OPEN_API_PATH_NOT_ALLOWED),表示该路径不对商户开放。

3. 公共请求头

HTTP 头必填说明
X-Access-Key-Id除健康检查外必填服务方分配的访问密钥 ID。
X-Merchant-Code除健康检查外必填您的商户编码,须与访问密钥所属商户一致。
X-Vcc-Timestamp除健康检查外必填Unix 时间戳,单位为(UTC);与网关时间偏差须在允许窗口内。
X-Vcc-Signature除健康检查外必填第 2 节所述 HMAC-SHA256 签名(小写十六进制)。
Idempotency-Key写操作必填POSTPUTPATCHDELETE 必须携带;建议使用 UUID。
Content-Type有 body 时使用 application/json
X-Trace-Id可选您侧生成的追踪 ID,便于排障。

4. 统一响应体(成功 / 失败)

4.1 成功

HTTP 状态码一般为 200。响应 JSON 外层结构如下:

{
  "code": "0",
  "message": "OK",
  "data": { ... 业务负载,随接口变化 ... },
  "traceId": "字符串,用于与服务方对账排障"
}

data 可为 JSON 对象、数组,或含 items 的分页对象。

4.2 失败

HTTP 状态码可为 4xx / 5xx。code 为业务错误码字符串,message 为人类可读说明,data 多为 nulltraceId 建议落库以便工单。

HTTPcode(示例)典型含义
400VALIDATION_ERROR参数缺失、格式非法、业务校验失败。
401OPEN_API_CREDENTIAL_INVALID访问密钥无效或已停用。
401OPEN_API_SIGNATURE_INVALID签名不匹配或缺少签名相关头。
401OPEN_API_CLOCK_SKEW时间戳超出允许偏差。
40140113 / OPEN_API_PATH_NOT_ALLOWED路径未对当前版本开放。
403FORBIDDEN商户编码与密钥不匹配、资源归属不符等。
404CARD_NOT_FOUNDWALLET_NOT_FOUND资源不存在或无权限访问。
409OPEN_API_IDEMPOTENCY_CONFLICT相同幂等键与已处理请求体不一致。
502CHANNEL_TRANSACTION_FETCH_FAILED渠道不可用(如实时拉取约束失败且无本地快照)。
429 / 503视配置限流或依赖不可用。

5. 幂等与写操作

所有写操作(POST / PUT / PATCH / DELETE)必须携带 Idempotency-Key。相同商户 + 相同键重复提交应返回首次成功语义。开卡任务当前按 Key 重放,不校验 body 哈希。网关按商户维度限流;PATCH .../spending-constraint 另有单独频率限制。

6. 接口一览

方法路径摘要
GET/open/v1/health健康检查(不要求签名头)
GET/open/v1/card-products已发布卡产品列表(含消费约束上限)
GET/open/v1/card-groups卡组列表(ACTIVE 自建;不含余额
POST/open/v1/card-groups创建自建卡组
DELETE/open/v1/card-groups/{id}软删除卡组
POST/open/v1/card-groups/{id}/fund主钱包 → 卡组子池
POST/open/v1/card-groups/{id}/release卡组子池 → 主钱包
GET/open/v1/wallets钱包列表(MAIN + 卡组子池)
GET/open/v1/wallets/{id}单个钱包余额
GET/open/v1/wallet-ledger资金流水(分页)
POST/open/v1/card-issuance-tasks创建开卡任务(可选归属卡组)
GET/open/v1/card-issuance-tasks/{id}查询开卡任务
GET/open/v1/cards卡片列表(分页,可按卡组筛选)
GET/open/v1/cards/{id}卡片详情(脱敏)
GET/open/v1/cards/{id}/sensitive卡片敏感信息(完整 PAN/CVV)
GET/open/v1/cards/{id}/spending-constraint读取当前消费约束
GET/open/v1/cards/{id}/spending-utilization周期额度使用情况
PATCH/open/v1/cards/{id}/card-group开卡后挂入/移出卡组
POST/open/v1/cards/{id}/actions卡片生命周期动作
PATCH/open/v1/cards/{id}/spending-constraint卡消费控制部分更新
GET/open/v1/transactions交易分页查询
GET/open/v1/slash-catalog/merchant-categories商户类别(MCC)目录
GET/open/v1/slash-catalog/merchants受理商户 ID 本地搜索
GET/open/v1/spending-constraint/reference消费约束静态参考(MCC/国家短列表)
GET/open/v1/mcc商户类别(别名,同 merchant-categories)
GET/open/v1/merchant-fee-rules/effective查询当前生效手续费规则及试算
说明:同一路径 /open/v1/card-groups 上,GET 为查询列表、POST 为创建,请以 HTTP 方法区分。仍不在本清单内的能力:钱包充值/提现/手工上账、API 凭据管理、持卡人 CRUD、开卡 body 自定义 spendingConstraintJson。充值与手工上账请使用 member-portal;API 凭据请使用商户门户或运营交付流程。

7. 接口明细(路径、请求、响应 data

下列「响应 data」均指统一外壳内 data 字段的形状。

7.1 健康检查

GET {MERCHANT_API_BASE}/open/v1/health

签名

不要求签名相关 HTTP 头。

响应 data

{ "status": "UP" }

7.2 已发布卡产品列表

GET {MERCHANT_API_BASE}/open/v1/card-products

响应 data

JSON 数组,每项字段示例:

[
  {
    "id": 1001,
    "code": "产品代码",
    "name": "产品名称",
    "currency": "USD",
    "published": true,
    "memberVisibility": "可见性枚举",
    "spendingConstraintCeilingJson": "{ ... 约束上限 JSON 字符串 ... }",
    "merchantCode": "M..."
  }
]

spendingConstraintCeilingJson(可选):卡产品配置的消费约束上限;调用 PATCH .../spending-constraint 时不得突破。

7.3 卡组列表

GET {MERCHANT_API_BASE}/open/v1/card-groups

查询参数

参数必填说明
memberUserId省略 = 商户主会员。

返回主会员名下 ACTIVE 自建卡组memberOwned=true)。不含子池余额;余额见 §7.8 钱包列表(walletRole=CARD_GROUP)。

响应 data

[
  {
    "id": 9,
    "code": "M32_abc...",
    "name": "测试",
    "memberOwned": true,
    "status": "ACTIVE",
    "merchantCode": "M..."
  }
]

7.4 创建卡组

POST {MERCHANT_API_BASE}/open/v1/card-groups

与 §7.3 共用路径,HTTP 方法为 POST。创建主会员名下新的自建卡组(服务端自动生成唯一 code)。

请求头

须带 Idempotency-Key。相同键重复提交返回同一卡组;同键不同 name 返回 409。

查询参数

memberUserId:可选。

请求体

{ "name": "营销专用卡组" }
字段必填说明
name卡组名称,1–128 字符,trim 后非空。

响应 data

{
  "id": 12,
  "code": "M32_a1b2c3...",
  "name": "营销专用卡组",
  "status": "ACTIVE",
  "memberOwned": true,
  "merchantCode": "M..."
}

7.5 删除卡组

DELETE {MERCHANT_API_BASE}/open/v1/card-groups/{id}

软删除(status=INACTIVE),列表接口不再返回。删除前须:① 无绑卡② 全币种子池余额为 0(须先调用 §7.7 release 划回主钱包,不会自动清余额)。

请求头

须带 Idempotency-Key

查询参数

memberUserId:可选。

响应 data

{
  "id": 12,
  "status": "INACTIVE",
  "merchantCode": "M..."
}
场景HTTP说明
仍有卡片400卡组内仍有卡片,请先移出(PATCH .../card-group
子池有余额400请先 POST .../release 划回主钱包

7.6 划入子池(主钱包 → 卡组)

POST {MERCHANT_API_BASE}/open/v1/card-groups/{id}/fund

请求头

须带 Idempotency-Key

查询参数

memberUserId:可选。

请求体

{
  "currency": "USD",
  "amount": "100.00"
}
字段必填说明
currencyUSD
amount十进制金额字符串,须 > 0。

响应 data

{
  "id": "8842",
  "reference": "wo_8842",
  "status": "SUCCEEDED",
  "type": "CARD_GROUP_FUND",
  "currency": "USD",
  "amountMinor": 10000,
  "amount": "100.00",
  "merchantCode": "M..."
}

主钱包余额不足时返回 400 WALLET_INSUFFICIENT_FUNDS。划款后可通过 §7.8 钱包、§7.9 流水确认。

7.7 划出子池(卡组 → 主钱包)

POST {MERCHANT_API_BASE}/open/v1/card-groups/{id}/release

请求头、查询参数、请求体与 §7.6 相同。

响应 data

{
  "id": "8843",
  "reference": "wo_8843",
  "status": "SUCCEEDED",
  "type": "CARD_GROUP_RELEASE",
  "currency": "USD",
  "amountMinor": 5000,
  "amount": "50.00",
  "merchantCode": "M..."
}

子池余额不足时返回 400 WALLET_INSUFFICIENT_FUNDS

7.8 钱包列表

GET {MERCHANT_API_BASE}/open/v1/wallets

查询参数

参数必填说明
memberUserId指定则返回该用户钱包;省略则返回商户下全部(1 个 MAIN + N 个 CARD_GROUP 子池)。

响应 data

[
  {
    "walletId": "54",
    "currency": "USD",
    "availableAmountMinor": 12122144,
    "availableAmount": "121221.44",
    "cardGroupId": 0,
    "walletRole": "MAIN",
    "cardGroupName": null,
    "merchantCode": "M..."
  },
  {
    "walletId": "55",
    "currency": "USD",
    "availableAmountMinor": 100000,
    "availableAmount": "1000.00",
    "cardGroupId": 9,
    "walletRole": "CARD_GROUP",
    "cardGroupName": "测试",
    "merchantCode": "M..."
  }
]

7.9 单个钱包

GET {MERCHANT_API_BASE}/open/v1/wallets/{id}

响应 data

与列表项相同结构(单对象)。

7.10 资金流水(分页)

GET {MERCHANT_API_BASE}/open/v1/wallet-ledger

查询参数

参数必填说明
memberUserId省略时使用商户主会员。
page / size默认 page=0size=20
sortascdesc,默认 desc

响应 data

{
  "total": 100,
  "page": 0,
  "size": 20,
  "sort": "desc",
  "merchantCode": "M...",
  "items": [ { "id": "1", "amountMinor": -500, "walletAccountId": "55", "cardGroupId": "9", ... } ]
}

7.11 创建开卡任务

POST {MERCHANT_API_BASE}/open/v1/card-issuance-tasks

请求头

须带 Idempotency-Key

请求体(仅允许下列字段)

{
  "cardProductId": 1001,
  "memberUserId": 2002,
  "platformCardGroupId": 9
}
字段必填说明
cardProductId已发布卡产品 ID。
memberUserId省略 = 商户主会员。
platformCardGroupId开卡后归属卡组;省略/null = 主钱包扣款。须为本人 ACTIVE 自建卡组;开卡前请通过 §7.6 向子池划入足够余额。

不接受任意 spendingConstraintJson 或其他未列字段。

响应 data

{
  "id": "任务ID",
  "merchantCode": "M...",
  "status": "SUCCEEDED",
  "channelRequestId": "...",
  "appliedFeeBreakdown": { "feeType": "CARD_ISSUANCE", "feeMinor": "...", "summary": "..." }
}

7.12 查询开卡任务

GET {MERCHANT_API_BASE}/open/v1/card-issuance-tasks/{id}

响应 data

{ "id": "...", "merchantCode": "M...", "status": "...", "channelRequestId": "..." }

7.13 卡片列表(分页)

GET {MERCHANT_API_BASE}/open/v1/cards

查询参数

参数必填说明
page / size默认 0 / 20size 最大 100。
cardGroupId不传 = 全部;0 = 未归属主钱包;>0 = 指定卡组。

响应 data

分页对象(非数组):

{
  "total": 42,
  "page": 0,
  "size": 20,
  "merchantCode": "M...",
  "items": [
    {
      "id": "卡账户ID",
      "displayName": null,
      "panMasked": "******4242",
      "panLast4": "4242",
      "status": "ACTIVE",
      "cardProductId": 1001,
      "platformCardGroupId": 9,
      "cardProductName": "产品名",
      "channelDisplayName": "VCC",
      "channelCode": "VCC",
      "cardGroupName": "测试",
      "cardGroupCode": "M32_...",
      "cardGroupMemberOwned": true,
      "cardGroupLabel": "我的卡组 · 测试",
      "merchantCode": "M..."
    }
  ]
}

7.14 卡片详情

GET {MERCHANT_API_BASE}/open/v1/cards/{id}

响应 data

与 §7.13 列表项结构一致的单对象;不包含完整 PAN/CVV。

7.15 卡片敏感信息

GET {MERCHANT_API_BASE}/open/v1/cards/{id}/sensitive

查询参数

参数必填说明
memberUserId若传入须与卡片归属用户一致,否则 403。

响应 data

{
  "cardId": "卡ID",
  "channelCode": "VCC",
  "pan": "完整卡号",
  "cvv": "CVV",
  "expMonth": 12,
  "expYear": 2029,
  "merchantCode": "M..."
}
该接口返回完整 PAN/CVV,请仅在必要场景调用,并确保传输与存储符合 PCI 相关要求。

7.16 读取卡消费约束

GET {MERCHANT_API_BASE}/open/v1/cards/{id}/spending-constraint

查询参数

memberUserId:可选。

适用于平台已开通消费约束能力的卡片。

响应 data

{
  "source": "LIVE",
  "spendingConstraint": { },
  "merchantCode": "M..."
}

source=LIVE 为实时拉取;渠道不可用时可能为 CACHED,并含 nonRealtimecachedAtchannelError

7.17 读取卡周期额度使用

GET {MERCHANT_API_BASE}/open/v1/cards/{id}/spending-utilization

查询参数

memberUserId:可选。

响应 data

{
  "cardAccountId": 25,
  "supported": true,
  "spentCents": 1200,
  "limitCents": 50000,
  "currency": "USD",
  "preset": "monthly",
  "periodKey": "2026-06",
  "utilizationPercent": 2,
  "source": "RECONCILE",
  "nonRealtime": true,
  "merchantCode": "M..."
}

7.18 开卡后挂入/移出卡组

PATCH {MERCHANT_API_BASE}/open/v1/cards/{id}/card-group

请求头

须带 Idempotency-Key

查询参数

memberUserId:可选。

请求体

{ "platformCardGroupId": 9 }

移出卡组(改回主钱包扣款):{ "platformCardGroupId": null }

仅可绑本人 ACTIVE 自建卡组;已开卡后不可改绑运营「平台卡组」。

响应 data

GET /cards/{id} 相同(含更新后的 cardGroupLabel 等)。

7.19 卡片生命周期动作

POST {MERCHANT_API_BASE}/open/v1/cards/{id}/actions

请求头

须带 Idempotency-Key

请求体

{
  "action": "ACTIVATE | FREEZE | UNFREEZE | CLOSE",
  "memberUserId": 2002
}

memberUserId 可选。

响应 data

{ "id": "卡ID", "merchantCode": "M...", "action": "FREEZE", "status": "FROZEN" }

7.20 卡消费控制部分更新

PATCH {MERCHANT_API_BASE}/open/v1/cards/{id}/spending-constraint

请求头

须带 Idempotency-Key

查询参数

memberUserId:可选。

请求体

与平台 spending-constraint 规范对齐;配置前建议调用 §7.24、§7.22、§7.23。不得突破卡产品 spendingConstraintCeilingJson

响应 data

更新后的消费控制 JSON。

7.21 交易分页查询

GET {MERCHANT_API_BASE}/open/v1/transactions

查询参数

参数必填说明
memberUserId省略时使用商户主会员。
cardId / txnType / from / to筛选条件。
page / size默认 page=0size=20

响应 data

{
  "total": 100,
  "page": 0,
  "pageSize": 20,
  "merchantCode": "M...",
  "items": [ { "id": "...", "amountMinor": -1200, "merchantCode": "M..." } ]
}

7.22 商户类别(MCC)

GET {MERCHANT_API_BASE}/open/v1/slash-catalog/merchant-categories

查询参数

cursor:可选,游标分页。

响应 data

商户类别 JSON(含 items、下一页 cursor 等)。

兼容别名(建议迁移至本路径):GET /open/v1/mcc,行为相同。

7.23 受理商户搜索

GET {MERCHANT_API_BASE}/open/v1/slash-catalog/merchants

查询参数

参数必填说明
q名称/ID 关键字。
page / size默认 0 / 20,最大 100。
ensureIds逗号分隔受理商户 ID,保证回显已选项。

响应 data

{
  "total": 1200,
  "source": "LOCAL_DB",
  "lastSyncAt": "2026-06-18T10:00:00Z",
  "items": [
    { "id": "ext_merchant_xxx", "name": "AMAZON", "label": "AMAZON (ext_merchant_xxx)" }
  ],
  "merchantCode": "M..."
}

7.24 消费约束静态参考

GET {MERCHANT_API_BASE}/open/v1/spending-constraint/reference

组装 PATCH .../spending-constraint 前的 MCC/国家短列表、周期枚举等静态参考。

响应 data

{
  "restrictions": ["allowlist", "blacklist"],
  "utilizationPresets": ["daily", "weekly", "monthly", "yearly", "collective"],
  "mccShortlist": [{ "code": "5411", "label": "5411 — 超市 / Grocery" }],
  "countryShortlist": [{ "code": "US", "label": "US — 美国" }],
  "fieldNotes": {
    "merchantCategoryRule": "商户类别 ID,见 merchant-categories",
    "merchantRule": "受理商户 ID,见 merchants 搜索"
  },
  "merchantCode": "M..."
}

7.25 查询生效手续费规则并试算

GET {MERCHANT_API_BASE}/open/v1/merchant-fee-rules/effective

查询参数

参数必填说明
feeTypeCARD_ISSUANCE / TRANSACTION / REFUND / MANUAL_TOP_UP / WITHDRAWAL
baseAmountMinor计费基数(最小货币单位整数)。

响应 data

{
  "feeType": "TRANSACTION",
  "source": "MERCHANT",
  "quotedFeeMinor": "30",
  "summary": "人类可读规则摘要",
  "merchantCode": "M..."
}

8. 商户 Webhook 出站通知(验签)

本节为 VCC → 商户 方向的 HTTP 回调,与 §2 Open API 入站签名 不是同一套算法,请勿使用 API SecretKey 验签 Webhook,亦勿用 Webhook Secret 调用 Open API。

对比项Open API(商户 → VCC)Webhook(VCC → 商户)
方向商户请求 /open/v1/**平台 POST 至商户配置的回调 URL
密钥API SecretKeyWebhook Secret(门户单独配置)
签名输入Canonical Request(4 行)timestamp + nonce + body 字符串拼接
X-Vcc-TimestampUnix Unix 毫秒
配置member-portal → Open API 凭证member-portal → Webhook 设置

8.1 配置与订阅

  1. 登录 member-portal,进入 Webhook 设置
  2. 填写回调 URL(生产须 HTTPS;禁止内网/metadata 等 SSRF 目标)。
  3. 订阅事件类型;保存后获得 Webhook Secret(仅创建/轮换时明文展示一次)。
  4. 使用「测试投递」验证连通性;「投递日志」可查询与手动重试失败记录。

配置接口位于 /api/v1/member/webhook-config(会员 JWT),不在 /open/v1 白名单。

8.2 回调 HTTP 约定

说明
方法POST
Content-Typeapplication/json; charset=utf-8
成功响应HTTP 2xx(建议 200);非 2xx 可能触发重试
超时平台侧连接/读超时约 5s / 10s(以部署为准)

8.3 签名 HTTP 头

HTTP 头说明
X-Vcc-SignatureHMAC-SHA256 签名,小写十六进制
X-Vcc-Timestamp签名时 Unix 时间戳,单位 毫秒(UTC)
X-Vcc-Nonce16 字节随机数的小写 hex(32 字符)
X-Vcc-Event-Id事件唯一 ID,须与 JSON 内 eventId 一致

8.4 验签算法

  1. 读取请求体原始字节,UTF-8 解码为字符串 body(勿重新序列化 JSON)。
  2. 读取头 X-Vcc-TimestampX-Vcc-NonceX-Vcc-Signature
  3. 拼接待签名字符串(无分隔符):payload = String(timestamp) + nonce + body
  4. 计算:expected = hex_lower( HMAC_SHA256( UTF8(WebhookSecret), UTF8(payload) ) )
  5. 恒定时间比较 expectedX-Vcc-Signature
payload   = timestamp + nonce + body
Signature = 小写十六进制( HMAC_SHA256( UTF8(WebhookSecret), UTF8(payload) ) )
建议(可选):校验 X-Vcc-Timestamp 与当前时间偏差(如 ±5 分钟);短期缓存 X-Vcc-Nonce 防重放。

8.5 事件类型

eventType说明
merchant.webhook.transaction.updated平台交易状态变更(清算/入账等;不含实时授权)
merchant.webhook.wallet.card_binding.verification_code钱包绑卡验证码就绪
merchant.webhook.test门户「测试投递」(联调)

同一 eventId 成功投递后平台不重复推送;商户侧仍建议以 eventId 做业务去重。

8.6 事件载荷

外层 JSON 结构:

{
  "eventType": "merchant.webhook.transaction.updated",
  "eventId": "mwh_evt_txn_abc123",
  "occurredAt": "2026-06-23T09:15:57.424Z",
  "merchantCode": "M20260618123735000001",
  "data": { }
}

交易变更 — data 示例

{
  "transactionId": "txn_abc123",
  "cardAccountId": 10001,
  "txnType": "CHANNEL",
  "status": "SETTLED",
  "amountMinor": 100,
  "currency": "USD",
  "occurredAt": "2026-06-23T09:15:57.424Z",
  "merchantReference": "your-ref-001",
  "channelReference": "abc123"
}

交易变更 — data 字段

字段类型说明
transactionIdstring平台侧交易 ID,格式 txn_{channelReference}
cardAccountIdnumber | null卡账户 ID;未关联到卡时可能为 null
txnTypestring交易类型(大写),见下表「txnType 码值」。
statusstring交易状态(大写),见下表「status 码值」。
amountMinornumber金额,最小货币单位(如 USD 为分);符号与平台记账一致。
currencystring币种,如 USD
occurredAtstring业务发生时间,ISO-8601 UTC。
merchantReferencestring | null商户侧单号/引用(若有)。
channelReferencestring平台侧渠道交易引用;与 GET /open/v1/transactions 及幂等去重关联。

txnType 码值

平台统一转为大写;缺省为 CHANNEL。本事件在交易清算/入账等后续阶段推送,不含实时授权。

码值含义Webhook 说明
CHANNEL渠道同步交易最常见;平台同步渠道交易后的默认类型。
AUTHORIZATION授权实时授权阶段不会推送本事件。
CAPTURE请款 / 捕获可能出现。
SALE / PURCHASE消费可能出现。
SETTLEMENT清算可能出现。
REFUND退款可能出现。
VOID撤销可能出现。
REVERSAL冲正可能出现。
FEE费用可能出现。
其他大写字符串扩展类型未在上表列出时按平台原值大写推送;请兼容未知值。

status 码值

平台将上游状态映射为大写枚举;未命中下表时按原值大写推送。本事件不推送 AUTHORIZED(实时授权)。

码值含义说明
SETTLED已清算 / 已入账交易已完成清算或入账
PENDING处理中尚未终态
DECLINED已拒绝交易被拒绝
FAILED失败处理失败
CANCELLED已取消交易已取消
RETURNED已退回资金或交易已退回
REFUNDED已退款已完成退款
REVERSED已冲正交易已冲正
AUTHORIZED已授权仅内部授权阶段;不会出现在本 Webhook 事件中
其他大写字符串扩展状态未映射时原样大写;请兼容未知值
data 中不含 memberUserId;请用 cardAccountId / channelReference 关联业务。实时授权阶段不会触发本事件。同一 channelReference 状态变更会复用稳定 eventId,成功投递后不会重复推送。

绑卡验证码 — data 示例

{
  "memberUserId": 32,
  "inboundId": 1782206824529,
  "walletId": 10,
  "cardAccountId": 10001,
  "bindingCode": "123456",
  "expiresAt": "2026-06-23T09:22:31.285Z",
  "channelCode": "VCC"
}
Webhook 回调 data.bindingCode 为完整验证码;门户投递日志 API 可能对验证码脱敏展示。

8.7 重试

商户响应平台行为
HTTP 2xx标记成功,不再重试
HTTP 5xx 或连接/超时指数退避自动重试(默认最多 3 次)
HTTP 4xx一般不再自动重试

请快速返回 2xx 并异步处理业务,避免长时间阻塞导致超时重试。

8.8 验签示例

Python

import hmac, hashlib

def verify_vcc_webhook(body_utf8: str, secret: str, headers: dict) -> bool:
    ts = headers.get("X-Vcc-Timestamp") or headers.get("x-vcc-timestamp")
    nonce = headers.get("X-Vcc-Nonce") or headers.get("x-vcc-nonce")
    sig = (headers.get("X-Vcc-Signature") or headers.get("x-vcc-signature") or "").lower()
    if not ts or not nonce or not sig:
        return False
    payload = f"{ts}{nonce}{body_utf8 or ''}"
    expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Node.js

import crypto from 'crypto';

function verifyVccWebhook(bodyUtf8, secret, headers) {
  const ts = headers['x-vcc-timestamp'];
  const nonce = headers['x-vcc-nonce'];
  const sig = (headers['x-vcc-signature'] || '').toLowerCase();
  if (!ts || !nonce || !sig) return false;
  const payload = `${ts}${nonce}${bodyUtf8 ?? ''}`;
  const expected = crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Java

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String payload = timestamp + nonce + (bodyUtf8 == null ? "" : bodyUtf8);
byte[] expected = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
// 与 HexFormat 解析的 X-Vcc-Signature 做 MessageDigest.isEqual 比较

参考实现:仓库 open-api-client-demo/src/webhook-signature.mjs;本地全链路测试:npm run test:webhook

9. 典型对接流程

9.1 开卡并归属卡组(API 全链路)

  1. POST /card-groups — 创建卡组(或 GET /card-groups 选用已有)
  2. POST /card-groups/{id}/fund — 从主钱包划入子池
  3. GET /wallets — 确认 MAIN 与 CARD_GROUP 子池余额
  4. GET /card-products — 确认 cardProductIdspendingConstraintCeilingJson
  5. POST /card-issuance-tasks{ "cardProductId", "platformCardGroupId" }
  6. GET /cards/{id} — 确认 cardGroupLabel

9.2 删除卡组

  1. PATCH /cards/{id}/card-group — 移出卡组内全部卡片(platformCardGroupId: null
  2. POST /card-groups/{id}/release — 划回全部子池余额
  3. DELETE /card-groups/{id} — 软删除

9.3 配置消费约束

  1. GET /spending-constraint/reference
  2. GET /slash-catalog/merchant-categories
  3. GET /slash-catalog/merchants?q=...
  4. GET /cards/{id}/spending-constraint
  5. PATCH /cards/{id}/spending-constraint
  6. GET /cards/{id}/spending-utilization

9.4 开卡后改挂卡组

  1. GET /card-groups
  2. PATCH /cards/{id}/card-group{ "platformCardGroupId": 9 }null 移出
  3. GET /cards/{id} — 确认展示字段