订阅webhook
DEEPayment 向平台运营为当前商户配置的商户级 HTTPS Webhook URL 发送订阅和账单事件。
Subscription Event 与其他商户 webhook 复用同一套商户 API Access Key、Secret Key、Header 和六行 HMAC-SHA256 签名协议。处理事件前必须先对 HTTP 原始 Body 字节验签。
事件是实时线索。通知延迟、乱序、本地状态冲突或事件未覆盖所需状态时,调用对应查询接口确认当前事实。
Webhook URL 未配置或已停用时,Subscription 和 Invoice 业务仍正常推进,但事件不会投递;待投递任务会保留,在配置重新启用后继续重试。
事件类型
| 事件 | 含义 |
|---|---|
subscription.active | Subscription 进入 ACTIVE。 |
subscription.past_due | Subscription 存在逾期账单。 |
subscription.canceled | Subscription 已取消。 |
subscription.unpaid | Subscription 进入 UNPAID。 |
subscription.failed | Subscription 建立失败。 |
invoice.payment_failed | Invoice 扣款失败。 |
invoice.paid | Invoice 已支付并关联 Payment Order。 |
invoice.voided | Invoice 已作废,不再针对它收款。 |
invoice.uncollectible | Invoice 未确认支付成功,自动收款已经终止。 |
当前不为保留状态 TRIALING、PENDING_CHECKOUT 和 ACTIVATING 发送事件,这些状态必须查询 Subscription。
HTTP Header
Content-Type: application/json
X-Access-Key: your_access_key
X-Timestamp: 1784023200
X-Signature: lowercase_hex_hmac_sha256
X-Body-Encryption: AES-256-GCM
X-Timestamp 是本次 HTTP 投递时间,使用 Unix 秒,重试时可以变化。只有商户启用 Body 加密时才携带 X-Body-Encryption,明文投递不携带该 Header。payload 的 occurredAt 是领域事件产生时间,使用 Unix 毫秒,重试时保持不变。
验签规则
根据 X-Access-Key 找到同一环境的 Secret Key,并使用实际在线路上传输的 HTTP Body 原始字节计算:
bodyHash = lowercase_hex(SHA256(rawBody))
stringToSign = "POST" + "\n"
+ escapedPath + "\n"
+ rawQuery + "\n"
+ accessKey + "\n"
+ timestamp + "\n"
+ bodyHash
signature = lowercase_hex(
HMAC-SHA256(secretKey, stringToSign)
)
escapedPath和rawQuery来自已配置的 Webhook URL;rawQuery不包含?,没有 Query 时使用空行。- 必须原样保留
rawQuery,不能重新排序、重新编码或丢弃空值。 accessKey和timestamp分别使用X-Access-Key、X-TimestampHeader 的原始值。- 校验时间窗口后,使用安全比较验证收到的签名与计算结果。
携带 X-Body-Encryption: AES-256-GCM 时,rawBody 是加密 envelope。必须先对 envelope 原始字节校验 X-Signature,再解密;未知加密 Header 或解密失败必须拒绝,不能回退为明文解析。重试可能使用新的时间戳和加密 nonce,因此 wire body、Body Hash 和签名可以变化;解密后的业务 payload 及其 eventId 保持稳定。
公共 Envelope
| 字段 | 类型 | 说明 |
|---|---|---|
eventId | string | 稳定的事件唯一号和商户幂等键。 |
merchantId | integer | DEEPayment 商户 ID。 |
eventType | string | 事件类型。 |
occurredAt | integer | 领域事件产生时间,Unix 毫秒。 |
所有 Subscription 和 Invoice 事件都包含已冻结的 paymentMethod,当前仅支持 CARD。
Subscription 状态事件
{
"eventId": "NTF_example_active",
"merchantId": 10001,
"eventType": "subscription.active",
"occurredAt": 1784023200000,
"statusVersion": 3,
"subscriptionNo": "sub_example_001",
"merchantSubscriptionNo": "membership_user_1001",
"planNo": "plan_example_001",
"customerNo": "cus_example_001",
"paymentMethod": "CARD",
"status": "ACTIVE",
"currentPeriodStart": 1782864000000,
"currentPeriodEnd": 1785542399000,
"nextBillingTime": 1785542400000,
"canceledTime": 0,
"cancelReason": ""
}
五类 Subscription 事件使用同一结构,status 分别为 ACTIVE、PAST_DUE、CANCELED、UNPAID、FAILED。statusVersion 只在同一个 subscriptionNo 内单调递增;商户只接受大于该 Subscription 已处理版本的事件。
Invoice 支付失败事件
{
"eventId": "NTF_example_failed",
"merchantId": 10001,
"eventType": "invoice.payment_failed",
"occurredAt": 1784023200000,
"invoiceNo": "inv_example_002",
"subscriptionNo": "sub_example_001",
"merchantSubscriptionNo": "membership_user_1001",
"amount": "199.00",
"paidAmount": "0",
"currency": "MXN",
"paymentMethod": "CARD",
"status": "PAYMENT_FAILED",
"dueTime": 1784023100000,
"attemptCount": 1
}
Invoice 作废与停止收款事件
invoice.voided 和 invoice.uncollectible 与 invoice.payment_failed 使用相同的 Invoice 身份和金额字段,status 分别为 VOID 和 UNCOLLECTIBLE。
{
"eventId": "NTF_example_voided",
"merchantId": 10001,
"eventType": "invoice.voided",
"occurredAt": 1784023200000,
"invoiceNo": "inv_example_003",
"subscriptionNo": "sub_example_001",
"merchantSubscriptionNo": "membership_user_1001",
"amount": "199.00",
"paidAmount": "0",
"currency": "MXN",
"paymentMethod": "CARD",
"status": "VOID",
"dueTime": 1784023100000,
"attemptCount": 0
}
{
"eventId": "NTF_example_uncollectible",
"merchantId": 10001,
"eventType": "invoice.uncollectible",
"occurredAt": 1784023200000,
"invoiceNo": "inv_example_004",
"subscriptionNo": "sub_example_001",
"merchantSubscriptionNo": "membership_user_1001",
"amount": "199.00",
"paidAmount": "0",
"currency": "MXN",
"paymentMethod": "CARD",
"status": "UNCOLLECTIBLE",
"dueTime": 1784023100000,
"attemptCount": 3
}
这些 Invoice 事件不改变 Subscription 合同状态,商户判断权益时应单独查询 Subscription。
Invoice 支付成功事件
{
"eventId": "NTF_example_paid",
"merchantId": 10001,
"eventType": "invoice.paid",
"occurredAt": 1784023200000,
"invoiceNo": "inv_example_001",
"subscriptionNo": "sub_example_001",
"merchantSubscriptionNo": "membership_user_1001",
"orderNo": "FP_example_001",
"merchantOrderNo": "SUBINV_example_001",
"amount": "199.00",
"paidAmount": "199.00",
"currency": "MXN",
"paymentMethod": "CARD",
"status": "PAID",
"payTime": 1784023199000,
"metadata": {
"planNo": "plan_example_001",
"customerNo": "cus_example_001"
}
}
invoice.paid 只确认 Invoice 支付,不代表已结算。当前 Subscription API 不公开结算状态。
商户处理顺序
- 定位凭据
缺失或未知的
X-Access-Key必须拒绝;根据 Access Key 找到 Secret Key,并保留实际 wire body 原始字节。 - 校验 wire 签名
校验 Unix 秒时间戳窗口,根据已配置 URL 重建六行 stringToSign,并在解析 Body 前验证
X-Signature。 - 严格解密
存在
X-Body-Encryption: AES-256-GCM时,解密已完成验签的 envelope;否则把已验签的 raw body 解析为 JSON。 - 幂等落库
使用 payload 中的
eventId做唯一键;重复事件不能重复开通权益或入账。处理 Subscription 事件时,还应拒绝不大于该 Subscription 已处理版本的statusVersion。 - 确认当前事实
不同事件类型不保证严格到达顺序;事件与本地状态冲突时查询对应对象。
- 返回 2xx
只有成功处理后才返回任意 HTTP 2xx,非 2xx 会触发平台重试。
投递语义为至少一次。商户已处理事件但 DEEPayment 未收到有效 2xx 时,同一个 eventId 可能再次投递。
查询 API 中的对象状态是整数,事件 payload 的 status 是字符串,两套契约必须分别解析。