Webhook
订单到达终态时,平台向你的 webhookUrl POST 一段明文 JSON,用平台 Ed25519 密钥签名。SDK 验证请求并返回解析后的事件,你不需要自己处理 header。线路格式见 API Reference 的代收 webhook和代付 webhook。
交给 SDK 的必须是收到的原始字节。任何在你的 handler 之前解析 JSON 的框架都会改变字节、破坏摘要。为 webhook 路由关闭 body 解析,或直接读原始流。
两类方法
| 只验证 | 验证并解析 | |
|---|---|---|
| Go | VerifyWebhook(r) ([]byte, error) | ParsePaymentWebhook(r)、ParsePayoutWebhook(r) |
| JavaScript | verifyWebhook({ method, path, headers, body, rawQuery }) | parsePaymentWebhook(...)、parsePayoutWebhook(...) |
| Python | verify_webhook(method=, path=, headers=, body=, raw_query=) | parse_payment_webhook(...)、parse_payout_webhook(...) |
| PHP | verifyWebhook($method, $path, $headers, $body, $rawQuery = '') | parsePaymentWebhook(...)、parsePayoutWebhook(...) |
| Java | verifyWebhook(method, path, headers, body, rawQuery) | parsePaymentWebhook(...)、parsePayoutWebhook(...) |
验证方法返回已验签的原始 body。解析方法在此基础上解码、检查五个必填字段并强制 orderType:把代付事件交给代收解析器会被拒绝,路由错的 webhook 不会更新错误的表。除非你自己路由事件,否则用解析方法。
检查顺序固定:请求外形、对 body 的 Content-Digest、Webhook-Event-Id 等于 body 的 eventId、created/expires 时间窗、按 keyid 选平台公钥、Ed25519 签名。任一失败即为 webhook 错误,回 4xx 且不处理 body。
Handler 示例
- Go
- JavaScript
- Python
- PHP
- Java
func paymentWebhook(w http.ResponseWriter, r *http.Request) {
wh, err := c.ParsePaymentWebhook(r) // 自己读取并验证 r.Body
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if seen(wh.EventID) { // 至少一次投递
w.WriteHeader(http.StatusOK)
return
}
switch wh.Status {
case deepayment.StatusSucceeded:
credit(wh.MerchantOrderNo, wh.PaidAmount)
case deepayment.StatusFailed:
fail(wh.MerchantOrderNo, wh.Failure.Msg)
}
w.WriteHeader(http.StatusOK)
}
// Express:只为这条路由保留原始 body
app.post('/webhook/payment', express.raw({ type: 'application/json' }), async (req, res) => {
let wh;
try {
wh = await client.parsePaymentWebhook({
method: req.method,
path: req.path,
rawQuery: req.url.split('?')[1] ?? '',
headers: req.headers,
body: req.body, // 原始字节的 Buffer
});
} catch (err) {
return res.sendStatus(401);
}
if (await seen(wh.eventId)) return res.sendStatus(200);
if (wh.status === 'SUCCEEDED') await credit(wh.merchantOrderNo, wh.paidAmount);
else if (wh.status === 'FAILED') await fail(wh.merchantOrderNo, wh.failure?.msg);
res.sendStatus(200);
});
# Flask
@app.post("/webhook/payment")
def payment_webhook():
try:
wh = client.parse_payment_webhook(
method=request.method,
path=request.path,
raw_query=request.query_string.decode(),
headers=dict(request.headers),
body=request.get_data(), # 原始字节
)
except WebhookError:
return "", 401
if seen(wh.eventId):
return "", 200
if wh.status == "SUCCEEDED":
credit(wh.merchantOrderNo, wh.paidAmount)
elif wh.status == "FAILED":
fail(wh.merchantOrderNo, wh.failure.msg if wh.failure else "")
return "", 200
try {
$wh = $client->parsePaymentWebhook(
$_SERVER['REQUEST_METHOD'],
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
getallheaders(),
file_get_contents('php://input'), // 原始字节
$_SERVER['QUERY_STRING'] ?? ''
);
} catch (WebhookException $e) {
http_response_code(401);
exit;
}
if (seen($wh['eventId'])) { http_response_code(200); exit; }
if ($wh['status'] === 'SUCCEEDED') credit($wh['merchantOrderNo'], $wh['paidAmount']);
elseif ($wh['status'] === 'FAILED') fail($wh['merchantOrderNo'], $wh['failure']['msg'] ?? '');
http_response_code(200);
// Servlet
Map<String, Object> wh;
try {
wh = client.parsePaymentWebhook(
request.getMethod(),
request.getRequestURI(),
headers, // Map<String, String>
request.getInputStream().readAllBytes(), // 原始字节
request.getQueryString() == null ? "" : request.getQueryString());
} catch (DeepaymentException.Webhook e) {
response.setStatus(401);
return;
}
if (seen((String) wh.get("eventId"))) { response.setStatus(200); return; }
if ("SUCCEEDED".equals(wh.get("status"))) credit(wh);
else if ("FAILED".equals(wh.get("status"))) fail(wh);
response.setStatus(200);
事件字段
| 字段 | 代收 | 代付 | 含义 |
|---|---|---|---|
eventId | 有 | 有 | 每个事件唯一,重试时不变。你的去重键。 |
orderType | PAYMENT | PAYOUT | 解析方法强制校验。 |
orderNo、merchantOrderNo | 有 | 有 | 两个单号。 |
status | 有 | 有 | 只有终态:SUCCEEDED、FAILED,代收还有 EXPIRED、CANCELED。 |
currency、amount | 有 | 有 | decimal 字符串。 |
paidAmount | 有 | 无 | 实际结算金额。 |
channelTradeNo | 可选 | 可选 | 渠道侧流水号。 |
failure | 失败时 | 失败时 | code、msg、message。 |
attach | 原样 | 原样 |
失败
status 为 FAILED 时按 failure.msg 分支;failure.message 是给日志和客服看的自由文本。
failure.msg | 含义 | 处置 |
|---|---|---|
ORDER_REJECTED | 风控或业务规则拒绝 | 不要用同一请求重试 |
CHANNEL_ERROR | 上游渠道失败 | 订单已终态;业务允许时新建一笔 |
INSUFFICIENT_BALANCE | 代付资金不足 | 充值后新建代付 |