生产运行
Webhooks
Signed, retried event deliveries: endpoints, event types, verification in four languages, testing and replay.
阅读约 6 分钟
开发者指南以英文撰写,以确保代码、字段名和错误信息与 API 完全一致。
本页内容
Endpoints
Create one or more endpoints with POST /partner/webhooks/endpoints (name, url, events as a comma-separated list or *, and mode). Each gets its own signing secret, shown once at creation. Update, delete and list them with the /partner/webhooks/endpoint… calls, or in the console.
Event types
| Event | Sent when |
|---|---|
charge.created | A prompt-to-pay charge was created. |
charge.completed | The customer approved and the money moved. |
charge.declined | The customer declined the prompt. |
charge.expired | The prompt expired unanswered. |
checkout.completed | A hosted checkout session was paid. |
c2b.confirmation | A customer paid your till or pay bill directly. |
What a delivery looks like
| Header | Value |
|---|---|
X-PesaBridge-Event | The event type, e.g. charge.completed. |
X-PesaBridge-Signature | Hex HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. |
X-PesaBridge-Timestamp | Unix time the delivery was signed. |
X-PesaBridge-Signature-V2 | t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">: replay-resistant. |
X-PesaBridge-Delivery | Delivery id: log it, and use it to ignore duplicates. |
Verify every delivery
Compute the HMAC over the exact bytes you received, before parsing JSON, and compare in constant time. Prefer V2 and reject timestamps older than five minutes.
import crypto from "node:crypto";
// Express: app.post("/webhook", express.raw({ type: "application/json" }), handler)
function verified(req, secret) {
const [t, v1] = (req.get("X-PesaBridge-Signature-V2") || "").split(",").map((p) => p.split("=")[1]);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${req.body}`).digest("hex");
return v1?.length === want.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}import hmac, hashlib, time
def verified(raw_body: bytes, headers, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in headers.get("X-PesaBridge-Signature-V2", "").split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1", "")
if not t or abs(time.time() - int(t)) > 300:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, want)<?php
$raw = file_get_contents("php://input");
parse_str(str_replace(",", "&", $_SERVER["HTTP_X_PESABRIDGE_SIGNATURE_V2"] ?? ""), $sig);
$ok = isset($sig["t"], $sig["v1"]) && abs(time() - (int)$sig["t"]) <= 300
&& hash_equals(hash_hmac("sha256", $sig["t"] . "." . $raw, getenv("PB_WHSEC")), $sig["v1"]);
http_response_code($ok ? 200 : 401);package webhook
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex")
// Verified checks the V1 signature: hex HMAC-SHA256 of the raw body.
func Verified(raw []byte, sig, secret string) bool {
m := hmac.New(sha256.New, []byte(secret))
m.Write(raw)
return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(sig))
}Retries and duplicates
Answer with any 2xx within 10 seconds. Anything else, or a timeout, is retried with exponential back-off (1, 2, 4, 8 and 16 minutes, up to 6 attempts), after which the delivery is marked failed. Because of retries, the same event can arrive twice: make your handler idempotent on X-PesaBridge-Delivery or on charge_ref.
We only deliver to public HTTPS addresses. Private and internal network addresses are refused.
Test and replay
POST /partner/webhooks/testsends a test event to an endpoint.GET /partner/webhooks/deliverieslists recent deliveries with their status code and attempts.POST /partner/webhooks/resendreplays a delivery after you fix your handler.- The Echo receiver (
GET /partner/echo/url) captures and auto-verifies deliveries with no server of your own.
我们的工程师解答集成问题。把请求参考号发给我们,我们会找到那次调用。
联系支持