Webhook 规范
Adopay 通过 Webhook(HTTP 回调)将支付、退款、代付、换汇、MED 等事件异步通知给接入方。本页定义所有回调共用的投递规则:回调地址配置、传输与超时、签名校验、成功响应、重试与幂等。各产品的事件类型与载荷字段以对应事件页为准:商户事件见 商户API 的 Webhooks 分组,合作商事件见 合作商API 的 Webhook 分组。
核心原则:Webhook 只是结果通知,不是事实源头。通知可能延迟、重复或乱序,订单的最终状态一律以订单查询接口返回为准;未收到通知时通过主动查询兜底。
1. 投递流程
sequenceDiagram
autonumber
participant PLAT as ADOPAY 平台
participant MER as 接入方接收端
PLAT->>MER: POST 事件载荷(携带签名请求头)
MER->>MER: 校验签名与时间戳
MER->>MER: 幂等检查(重复事件直接确认)
MER-->>PLAT: 3 秒内返回 2xx(建议 SUCCESS)
Note over MER: 业务处理(入账、发货等)在确认之后异步执行
alt 超时 / 非 2xx / 响应体不符合要求
PLAT->>MER: 自动重试(同一事件可能多次投递)
end2. 回调地址配置
回调地址通过以下两种方式之一提供给 Adopay,不同产品相互独立、分别配置:
| 方式 | 适用场景 | 说明 |
|---|---|---|
| 商户后台配置 | Merchant API 订单回调、MED 通知 | 在商户后台(或联系运营)为对应产品配置固定回调地址;接口文档中以 /merchant-callback-url 占位表示。 |
| 请求参数指定 | Merchant API / Partner API | 收款退款请求使用 notifyUrl,未上送时使用商户已配置且启用的退款通知地址;其他场景的字段名和未填写时的行为以各接口文档为准。 |
地址要求:
- 必须是完整的 HTTPS 地址(含协议与域名)。
- 公网可达,不能是仅内网或本地开发环境地址。
- 接收端须在 3 秒内完成响应(见第 5 节),地址对应的后端服务需满足该性能要求。
- 沙箱与生产环境分别配置并各自联调;生产上线前必须先完成 Webhook 联调(见接入指南)。
3. 公共投递规则
| 规则 | 要求 |
|---|---|
| 传输 | HTTPS 回调地址 |
| 请求方式 | POST,Content-Type: application/json |
| 超时 | 3 秒内完成响应 |
| 成功判定 | 3 秒内返回 HTTP 200-299;部分产品还要求响应体为 SUCCESS / OK(见第 5 节) |
| 失败处理 | 超时、非成功响应或连接失败视为投递失败,进入自动重试 |
| 幂等 | 同一事件可能多次投递,接收方必须可安全处理重复 |
| 验签 | 必须校验平台签名,验签通过前不得处理业务 |
4. 请求头与签名校验
Adopay 投递回调时携带以下请求头:
| 请求头 | 必有 | 说明 |
|---|---|---|
X-Merchant-Id | 是 | 接收通知的商户号。 |
X-Timestamp | 是 | Unix 秒级时间戳;接收方应校验与当前时间的偏差(建议 ±300 秒),拒绝过期请求。 |
X-Nonce | 是 | 本次投递的随机串,用于防重放;有效期内不应重复出现。 |
Digest | 是 | 请求体摘要,格式 SHA-256=<Base64(SHA-256(原始body字节))>。 |
Authorization | 是 | 平台 ES256 签名信息,使用平台公钥验签(当前 keyId 固定为 v1)。 |
X-Secret-Key-Version | 否 | 签名密钥版本,系统获取到版本时携带。 |
X-Trace-Id | 否 | 平台链路追踪号,排查问题时提供给 Adopay。 |
验签步骤:
- 用原始请求体字节重新计算摘要,与
Digest头比对;不要先反序列化 JSON 再重新序列化。 - 校验
X-Timestamp在允许时间窗内、X-Nonce未重复出现(防重放)。 - 按 请求签名第 3、4 节的签名串规则,使用平台公钥验证
Authorization中的 ES256 签名。 - 任一步失败:不要处理业务,返回非
2xx让 Adopay 重试,并记录日志排查。
Python 验签示例:
import base64
import hashlib
import re
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
def verify_webhook(path, raw_query, headers, raw_body, platform_public_key_pem):
# 1. 校验 Digest:必须基于原始请求体字节计算
expected_digest = "SHA-256=" + base64.b64encode(hashlib.sha256(raw_body).digest()).decode()
if expected_digest != headers["Digest"]:
return False
# 2. 重建签名串(与请求签名规范一致)
request_target = "post " + path + ("?" + raw_query if raw_query else "")
canonical = "\n".join([
f"(request-target): {request_target}",
f"x-timestamp: {headers['X-Timestamp']}",
f"x-nonce: {headers['X-Nonce']}",
f"digest: {headers['Digest']}",
])
# 3. 解析 Authorization 中的签名,使用平台公钥验签
signature_b64 = re.search(r'signature="([^"]+)"', headers["Authorization"]).group(1)
public_key = serialization.load_pem_public_key(platform_public_key_pem.encode())
try:
public_key.verify(base64.b64decode(signature_b64), canonical.encode(), ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False时间窗校验与 nonce 去重需接入方自行实现。验签失败的事件一律丢弃,不要降级为"仅记录不校验"。
5. 成功响应要求
Adopay 判定投递成功的最低要求:3 秒内收到 HTTP 200-299。部分产品(如代付)在此基础上还要求响应体去除首尾空白后严格等于 SUCCESS 或 OK(区分大小写)。为同时满足所有产品,建议接收方统一返回纯文本:
HTTP/1.1 200 OK
Content-Type: text/plain
SUCCESS处理顺序建议:验签 -> 幂等检查 -> 事件持久化(落库或入队)-> 立即返回 SUCCESS。入账、发货等业务逻辑放在响应之后异步执行,绝不能挤占 3 秒响应窗口。
6. 重试、重复与顺序
- 重试:投递失败后Adopay 自动重试,次数与间隔以对应产品事件页为准(如代付通知首次失败后最多重试 9 次,合计最多投递 10 次;其余产品按"可能多次重复投递"设计)。
- 重复:重试和网络抖动都可能导致同一事件多次到达。建议以"业务对象 ID + 事件类型"构成幂等键(订单类事件可用商户号 +
platOrderNo+event,MED 事件可用medId+event),重复事件确认后直接返回成功。 - 顺序:不承诺按业务发生顺序投递,同一订单可能先后收到不同状态的事件。处理时应做状态机判断,避免旧状态覆盖新状态;无法确定时回查订单接口,不要凭通知推断终态(见幂等)。
7. 事件载荷结构
交易通知的业务状态字段
收款、收款退款、付款、付款退款和换汇 Webhook 的通知 JSON 顶层包含以下业务状态字段,与 event 同级,不额外嵌套 data。这组字段不适用于 MED 通知;MED 的 status 是流程状态字符串,不能作为整数响应码判断。
| 字段 | 类型 | 说明 |
|---|---|---|
status | int | 响应码 |
msg | string | 与 status 对应 |
status 为 int 类型,status = 200 表示业务正常,此时 msg = "sucesso";非 200 表示业务错误,错误原因见 msg。应按整数状态码判断,不能按字段是否有值判断;字段缺失、为空或类型不正确时不能判定为业务正常。业务正常不代表交易已经完成,仍需结合 event、orderStatus 等业务字段判断处理结果。Webhook 中的 status、msg 表示通知所携带的业务处理结果;查询接口的外层 status、msg 表示接口响应结果。业务状态字段不影响接收方按第 5 节要求确认已接收通知。
生产载荷分两类,字段以各产品事件页为准:
- 订单事件(支付 / 退款 / 代付 / 换汇):扁平 JSON,
event标识事件类型,附订单字段:
{
"event": "PIX_QRCODE_PAID",
"status": 200,
"msg": "sucesso",
"merchantOrderNo": "20220721144249483",
"platOrderNo": "3KITxA1dEXhZ2IRXvwck",
"orderStatus": "SUCCESS",
"amount": 10000,
"payAmount": 10000
}(节选支付事件部分字段,完整字段见 支付 Webhook 页。)
- MED 事件:
event标识事件类型,载荷包含 查询MED详情data的全量字段及事件专属字段,不使用普通 API 的status、msg、data响应外层。以下仅节选部分字段:
{
"event": "MED_APPROVED",
"medId": "medc2874510938274639021",
"occurredAt": "2026-01-16T10:30:00Z",
"status": "ACCEPTED_BY_PSP",
"amount": 1000.50,
"analysisResult": "ACCEPTED"
}(示例,各事件的字段与触发时机见 MED Webhook。)
推荐信封(新事件)
新增事件类型推荐采用统一信封,便于接入方做通用路由与幂等:
{
"eventId": "evt_xxx",
"eventType": "PAYMENT.SUCCEEDED",
"status": 200,
"msg": "sucesso",
"createdAt": 1786190400,
"data": {}
}Partner 信封
Partner 场景的信封在 data 外额外携带 partnerId 与 merchantId:
{
"eventId": "evt_xxx",
"eventType": "PAYMENT.SUCCEEDED",
"status": 200,
"msg": "sucesso",
"partnerId": "P10001",
"merchantId": "M20001",
"data": {
"paymentId": "PAY10001",
"merchantOrderNo": "ORDER10001",
"amount": 100.00,
"currency": "BRL",
"status": "SUCCEEDED"
}
}Partner 平台应按 merchantId 将事件路由给下游商户。
8. 安全要求
- 先验签后处理:签名校验是唯一强校验手段,不要以来源 IP 白名单替代,也不要在验签失败时降级处理。
- 保护敏感信息:回调可能包含收款人姓名、证件号、Pix Key 等敏感数据,日志与存储应脱敏。
- 密钥管理:回调验签使用平台公钥(随开户材料提供);Adopay 轮换签名密钥时通过
keyId/X-Secret-Key-Version标识版本。
9. 接收端实现清单
10. 故障排查
| 现象 | 排查方向 |
|---|---|
| 收不到通知 | 回调地址是否公网可达、HTTPS 证书是否有效;收款退款请求中的 notifyUrl 或商户已配置且启用的退款通知地址是否正确,其他场景按对应接口文档核对;网关 / 防火墙是否放行 Adopay 请求。 |
| 验签失败 | 是否基于原始请求体字节计算摘要;签名串四行顺序与请求头值是否一致;时间戳是否在允许窗口内。 |
| 持续收到重试 | 是否 3 秒内返回 2xx;严格要求响应体的产品是否返回了纯文本 SUCCESS / OK(区分大小写)。 |
| 收到重复通知 | 属正常现象(重试机制),确认幂等处理是否生效。 |
排查时可保留 X-Trace-Id 并联系 Adopay协助定位。