商户 API 接入指南
通过 CardV 版本化 REST API 接入数字商品;完整字段以所选环境的 OpenAPI 为准。
https://b2b.cardv.net 正式环境 · https://sandbox.cardv.net 沙盒 · /api/v1
先在沙盒联调。 两个环境的 API Key、钱包、SKU、订单和供应商访问相互隔离;正式订单可能扣除真实资金并采购。
Live API reference ↗ · Sandbox API reference ↗ · Live OpenAPI schema ↗ · Merchant Portal ↗
接入步骤
- 申请商户账号、验证邮箱并等待审核;获批后登录 商户后台。Merchant ID 是不可变的
M########标识,不是公司名或邮箱。 - 在后台顶部选择 Live 或 Sandbox,进入 Integrations → API keys。创建密钥后点击 Reveal,通过邮件验证码查看并保存明文。
- 在所选环境调用
GET /api/v1/account和GET /api/v1/balance。 - 读取 SKU、获取最新报价,使用唯一商户订单号下单。
- 查询订单或接收已验签 Webhook,直到获得最终履约状态。
认证与 HMAC 签名
所有服务端请求需传 X-Merchant-Id 和 X-Api-Key。API Key 认证的 POST、PUT、PATCH、DELETE 请求还需 X-Timestamp、X-Nonce、X-Signature。时间戳为 Unix 秒,允许前后五分钟;Nonce 在窗口内只能用一次。
canonical = "\n".join([
METHOD.upper(),
PATH_WITH_QUERY,
TIMESTAMP,
NONCE,
sha256(EXACT_RAW_BODY).hexdigest()
])
signature = hmac_sha256(API_KEY, canonical).hexdigest().lower()对实际发送的原始请求体字节求 SHA-256,路径包含查询参数但不含域名。以 API Key 明文作 HMAC-SHA256 secret,签名为小写十六进制。重试时更新 timestamp 和 nonce。Swagger 不会自动生成写请求签名。
若配置 IP 白名单,来源 IP 也必须符合。不要记录 API Key 或完整交付数据。
商品目录与报价
| Endpoint | Purpose / 用途 |
|---|---|
GET /api/v1/account | Merchant account / 商户账号 |
GET /api/v1/balance | Wallet balance / 钱包余额 |
GET /api/v1/products | Product catalog / 商品目录 |
GET /api/v1/skus | Orderable SKUs / 可下单规格 |
GET /api/v1/skus/{sku_id}/quote | Current quote / 当前报价 |
POST /api/v1/orders | Create order / 创建订单 |
GET /api/v1/orders/{order_id} | Order detail / 订单详情 |
GET /api/v1/orders/by-external-id/{external_order_id} | Lookup by merchant reference / 按商户订单号查询 |
仅对 availability=available 的 SKU 下单。固定面值报价传 quantity;范围面值另传 face_currency 币种下、介于 min_face_value 与 max_face_value 之间的 amount。用报价返回的 merchant_price 作为 expected_unit_price;不要从面额或列表预览价推算。
GET /api/v1/skus/S000001/quote?quantity=1
GET /api/v1/skus/S000002/quote?quantity=1&amount=25.00下单、幂等与交付
POST /api/v1/orders 接收 external_order_id 与 items。每项包含 sku_id 和 quantity;建议传最新报价的 expected_unit_price。范围面值传 amount,直充商品传符合 required_input_schema 的 inputs。
{
"external_order_id": "YOUR-ORDER-10001",
"items": [{
"sku_id": "S000001",
"quantity": 1,
"expected_unit_price": "9.2500"
}]
}示例 SKU 和价格仅供说明,实际值须取自所选环境。external_order_id 在商户内唯一,长度 1–120 字符。新订单返回 201;相同内容重放返回 200 与 idempotent_replay=true;同一参考号不能改订单内容。
保存公开 order.order_id(O-...),不要依赖旧版数字 id。HTTP 下单成功不是交付成功。可用订单号或 external_order_id 查单;常见状态包括 accepted、processing、succeeded、partially_succeeded、failed、cancelled、refunded。履约失败不等于钱包已退款,须核对交易流水。API Key 查单详情可能返回完整卡密,请禁用缓存与敏感日志。
手机充值是独立流程
不要通过普通 /api/v1/orders 提交手机充值。先调用 GET /api/v1/recharge/countries 和 GET /api/v1/recharge/operators?country=...,随后 POST /api/v1/recharge/quote(国家、operator_key、amount、可选 subtype)。使用 quote_token、收款账号 account、金额和唯一 external_order_id 调用 POST /api/v1/recharge/orders,再通过 GET /api/v1/recharge/orders/{order_id} 查单。
Webhooks
Portal owner 在所选环境注册 HTTPS 端点。创建或重置时仅返回一次 signing_secret,列表和详情不再显示。CardV 发送 X-CardV-Event、X-CardV-Delivery、X-CardV-Timestamp 与 X-CardV-Signature: t=TIMESTAMP,v1=HEX。
expected = hmac_sha256(
WEBHOOK_SECRET,
X_CARDV_TIMESTAMP + "." + EXACT_RAW_BODY
).hexdigest().lower()用常数时间比较签名,拒绝过期时间戳,先持久化再返回 2xx。按 delivery ID 去重,也按业务订单号和状态处理重放。Webhook 不含完整卡密;需认证查单。
错误与安全重试
| HTTP | 处理 |
|---|---|
| 400 | 校验、价格或输入错误;修正请求。 |
| 403 + CardV JSON | 检查凭据、签名、权限和来源 IP。 |
| 403 + Cloudflare 1010 | 边缘拦截,请求未到达 CardV;换密钥无效。 |
| 404 | 资源不存在或不可见。 |
| 429 | 遵循 Retry-After。 |
| 5xx / timeout | 先按原 external_order_id 查单;仍不确定时保持原订单内容和参考号,只更新签名时间戳、Nonce 重试。 |
环境与当前限制
统一在正式商户后台登录,再通过顶部切换 Sandbox;沙盒不能直接用正式密码登录。各环境有独立的 OpenAPI、API Key、钱包、SKU 和订单。获配沙盒账号含 1,000 USD 非现金测试额度和有限测试目录,不支持真实充值。
Cloudflare 目前可能以 HTTP 403 / error 1010 拦截部分非浏览器客户端,且请求不会到达 CardV。健康检查或 Swagger 页面可访问并不证明实际 API 客户端可用。请先用你的实际客户端验证,切勿改用正式环境发送测试单。
正式低额试单请先与 CardV 对接人员协调。不要通过邮件传送密钥、Webhook secret 或卡密。