支付 API
CoinPay API 用来创建支付、获取托管收银台地址,并接收带签名的 webhook 事件。一个订单对应一笔支付。接口在本地和线上环境都可以联调。
认证
在服务端请求里带上 X-Api-Key。不要把 Key 放进前端代码、截图或客服对话。
X-Api-Key: cp_live_...
| 前缀 | 环境 | 用途 |
|---|---|---|
cp_live_ | 生产 | 真实收款和线上 webhook 事件。 |
cp_test_ | 沙箱 | 隔离的测试支付,不产生真实资金。 |
幂等
在 POST /payments 时带上 Idempotency-Key。Key 按商户隔离,重复提交会返回同一笔支付,避免重复扣款。
Idempotency-Key: order-1042
创建支付
POST /payments
amount 用 JSON 数字提交。merchantOrderId 是你的订单唯一参考号。返回结果里包含托管收银台地址 checkoutUrl。
curl -X POST http://127.0.0.1:8791/api/v1/payments -H 'X-Api-Key: cp_live_...' -H 'Idempotency-Key: order-1042' -H 'Content-Type: application/json' -d '{"amount":149.00,"currency":"USD","merchantOrderId":"order-1042","customer":{"email":"buyer@example.com"},"callbackUrl":"https://your-store.com/webhook"}'{
"ok": true,
"payment": {
"id": "ord_...",
"orderNo": "CP-...",
"status": "pending",
"amount": 149.00,
"currency": "USD",
"commissionBps": 3000,
"commissionCents": 4470,
"merchantNetCents": 10281
},
"checkoutUrl": "http://127.0.0.1:8791/pay/ord_...?token=..."
}| 字段 | 类型 | 说明 |
|---|---|---|
amount | number | 收款金额,例如 149.00。 |
currency | string | 商户收款目前支持 USD。 |
merchantOrderId | string | 你的唯一订单参考号。 |
customer.email | string | 可选,用于收据。 |
callbackUrl | string | 接收 webhook 的服务端地址。 |
commissionBps=3000 表示 30% 服务费;commissionCents 和 merchantNetCents 是这笔支付的分账金额。
查询支付
GET /payments/:id
用创建接口返回的支付 ID 查询状态。status 会从 pending 变为 paid。
curl http://127.0.0.1:8791/api/v1/payments/ord_... -H 'X-Api-Key: cp_live_...'
{
"ok": true,
"payment": {
"id": "ord_...",
"status": "paid"
}
}收款链接
POST /links
用固定金额和描述创建托管收款链接,返回一个可以直接发出去的 URL。
curl -X POST http://127.0.0.1:8791/api/v1/links -H 'X-Api-Key: cp_live_...' -H 'Content-Type: application/json' -d '{"amount":49.00,"description":"Order 1043"}'{
"ok": true,
"link": {
"id": "lnk_...",
"url": "http://127.0.0.1:8791/p/...",
"status": "open"
}
}Webhook 验签与事件
CoinPay 用商户 webhook secret 对原始请求体做 HMAC-SHA256 签名。签名放在 X-Coinpay-Signature,事件名放在 X-Coinpay-Event,投递 ID 放在 X-Coinpay-Delivery。一次支付可能收到四个事件,处理逻辑必须按事件分别判断。
X-Coinpay-Signature: sha256=HMAC_SHA256(secret, raw_body) X-Coinpay-Event: payment.succeeded X-Coinpay-Delivery: <orderId>-<attempt>
四个事件
事件共用同一份 payload schema;差异在 event 与 status。不要只看 HTTP 200,也不要只凭浏览器跳转判断支付成功。
← 可横向滚动查看完整事件表 →
| 事件 | 触发时机 | payload.status | 处理 |
|---|---|---|---|
payment.created | 订单创建成功并返回 checkoutUrl 后;记录待支付订单,不履约。 | pending | 记录待支付订单,不履约。 |
payment.succeeded | 支付确认到账后;验签后幂等履约。 | paid | 验签后幂等履约。 |
payment.failed | 建单失败或处理失败,订单不可继续支付;不要履约。 | failed | 不履约;新支付另开订单。 |
payment.underpaid | 实付低于订单金额 99%;不要按已付订单履约。 | underpaid | 不履约;标记少付并处理。 |
Payload 字段
四个事件都包含以下字段;event 与 status 随事件变化。
| 字段 | 类型 | 说明 |
|---|---|---|
id / orderId | string | CoinPay 订单 ID;同一订单的多个事件使用同一个 ID。 |
orderNo | string | 面向人的订单号,CP- 开头。 |
event | string | 事件名:payment.created / succeeded / failed / underpaid。 |
status | string | 事件状态:pending / paid / failed / underpaid。 |
amount / amountCents | number | 订单金额;美元与最小货币单位。 |
currency | string | USD。 |
commissionBps | number | 创建时锁定的佣金比例,3000 = 30%。 |
commissionCents | number | 平台佣金,单位分。 |
merchantNetCents | number | 商户净额,单位分。 |
四个事件的 payload 示例
{
"id": "ord_...",
"orderId": "ord_...",
"orderNo": "CP-...",
"amount": 149.0,
"amountCents": 14900,
"currency": "USD",
"commissionBps": 3000,
"commissionCents": 4470,
"merchantNetCents": 10281,
"status": "pending",
"event": "payment.created"
}{
"id": "ord_...",
"orderId": "ord_...",
"orderNo": "CP-...",
"amount": 149.0,
"amountCents": 14900,
"currency": "USD",
"commissionBps": 3000,
"commissionCents": 4470,
"merchantNetCents": 10281,
"status": "paid",
"event": "payment.succeeded"
}{
"id": "ord_...",
"orderId": "ord_...",
"orderNo": "CP-...",
"amount": 149.0,
"amountCents": 14900,
"currency": "USD",
"commissionBps": 3000,
"commissionCents": 4470,
"merchantNetCents": 10281,
"status": "failed",
"event": "payment.failed"
}{
"id": "ord_...",
"orderId": "ord_...",
"orderNo": "CP-...",
"amount": 149.0,
"amountCents": 14900,
"currency": "USD",
"commissionBps": 3000,
"commissionCents": 4470,
"merchantNetCents": 10281,
"status": "underpaid",
"event": "payment.underpaid"
}失败与少付处理
| 情况 | 建议处理 |
|---|---|
payment.created | 记录 checkout URL 和订单状态,不履约;客户完成支付前继续保持 pending。 |
payment.succeeded | 验签通过后按 id + event 做幂等,再履约/记账;重复投递不能重复发货。 |
payment.failed | 订单已失败,不履约;不要让客户继续在原 checkout 重试,必要时创建新支付并保留原订单审计记录。 |
payment.underpaid | 到账低于订单金额 99%,不按成功订单履约;在后台标记少付并联系客户补款,或创建新订单,不要把 underpaid 当作 paid。 |
验签必须使用原始请求体。失败重试 3 次,间隔 150ms、300ms,每次投递都记录在 webhook_deliveries。
const crypto = require('crypto');
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
if (expected !== req.headers['x-coinpay-signature']) {
throw new Error('Invalid CoinPay signature');
}错误码
错误响应会带稳定的 code 和可读说明。给客户展示重试或联系支持的入口,不要把原始服务端错误直接抛给客户。
| HTTP | 错误码 | API 返回 |
|---|---|---|
| 400 | invalid_amount | amount must be positive |
| 400 | amount_out_of_limits | amount is outside merchant limits |
| 400 | invalid_method | unsupported payment method |
| 401 | missing_api_key | X-Api-Key is required |
| 401 | invalid_api_key | invalid API key |
| 403 | merchant_inactive | merchant is paused |
| 403 | ip_not_allowed | request IP is not allowlisted |
| 404 | payment_not_found | payment not found |
| 429 | rate_limited | too many requests |
| 429 | daily_limit_reached | merchant daily limit reached |
Sandbox 完整流程
沙箱使用 cp_test_ Key,与生产隔离。测试支付不产生真实资金,但会走和线上相同的签名与重试逻辑。
- 申请 / 复制 test key:商户审批后登录商户门户「API 与 Webhook」,复制 Sandbox API Key(
cp_test_)和 Webhook 签名密钥(同一 secret 也用于沙箱事件)。入口不明显时,在门户申请沙箱权限,或通过 Telegram 联系客服开通测试凭据。 - 配置沙箱 webhook:在门户把接收地址指向你的测试端点(可用 webhook.site 或 ngrok),保存后确认最近投递状态。
- 创建测试支付:用 Sandbox Key 调用
POST /api/v1/sandbox/payments,拿到order id和checkoutUrl。 - 模拟完成 / 失败:调用
.../complete模拟成功,或.../fail模拟失败;不要用真实卡或真实资金。 - 核对事件:创建后应收到
payment.created;完成后应收到payment.succeeded;失败后应收到payment.failed。用 Webhook 签名密钥(同一 secret 也用于沙箱事件)验签,并在门户查看投递日志。
沙箱凭据
每个商户会从门户拿到以下测试凭据:
- Sandbox API Key:认证
cp_test_请求。 - Webhook 签名密钥:验证沙箱事件签名;与正式环境共用同一个 secret,也用于沙箱事件。
第一步:创建测试支付
curl -X POST http://127.0.0.1:8791/api/v1/sandbox/payments \
-H 'X-Api-Key: cp_test_...' \
-H 'Content-Type: application/json' \
-d '{"amount":7.00,"currency":"USD","merchantOrderId":"sandbox-001"}' 第二步:complete 或 fail
curl -X POST http://127.0.0.1:8791/api/v1/sandbox/payments/ord_.../complete \ -H 'X-Api-Key: cp_test_...'
curl -X POST http://127.0.0.1:8791/api/v1/sandbox/payments/ord_.../fail \ -H 'X-Api-Key: cp_test_...'