Webhooks
Last updated: 2026-07-17
Webhooks notify your system about payment order status updates.
Configuration
Webhook URL is configured for your merchant by platform administrators. Coordinate setup or changes through your platform support/operations process.
Delivery rules (actual)
- Method:
POST - Content-Type:
application/json - Timeout: up to 30 seconds per attempt
- Any 2xx response acknowledges the delivery, including
200,202, and204. - Network errors and HTTP
408,425,429, and5xxresponses are retried. Retries use exponential backoff with jitter until the delay reaches about one hour, then continue on roughly hourly intervals with jitter, up to 100 attempts. - A valid
Retry-Aftervalue on a retryable response can extend the delay. Both delta-seconds and HTTP-date formats are accepted; the applied delay is capped at 24 hours. - Other non-2xx responses, including redirects and most
4xxresponses, are permanent rejections and are not retried. Redirects are not followed.
Headers
Content-Type: application/jsonIdempotency-Key: <delivery_id>X-Webhook-Signature: v=1, t=<unix_timestamp>, alg=hmac-sha256, s=<hex_signature>
Payload
Webhook body is a PaymentOrder object (same
shape as GET /merchant/api/v1/payment/{id}).
Treat Idempotency-Key as an opaque delivery identifier. Most automated status updates use a payment/update-time based key; manual or replayed deliveries can use a different delivery ID.
The API may send repeated or corrective updates for the same payment. For example, PENDING -> PENDING can repeat, and a later confirmation can update FAILED to COMPLETED. Use Idempotency-Key to deduplicate deliveries safely and use the latest accepted paymentOrder.status for reconciliation.
Signature verification
The signature is not encrypted data and there is nothing to decrypt. The header contains metadata plus a hexadecimal HMAC digest:
| Part | Meaning |
|---|---|
v=1 | Signature format version |
t=<unix_timestamp> | Signing time in Unix seconds |
alg=hmac-sha256 | HMAC algorithm |
s=<hex_signature> | Lowercase hexadecimal SHA-256 HMAC digest |
To verify it:
- Capture the exact request-body bytes before JSON parsing, whitespace changes, character decoding, or reserialization.
- Split the header on commas, then split each part on the first
=. Requirev=1,alg=hmac-sha256, an integert, and a 64-character hexadecimals. - Base64-decode the configured webhook secret once. The decoded bytes are the HMAC key; the base64 text itself is not the key.
- Build the signed bytes as the ASCII timestamp, one literal dot, and the exact raw request body.
- Compute HMAC-SHA256, encode the result as lowercase hex, and compare it with
susing a constant-time comparison. - Reject timestamps outside your replay window. Keep system clocks synchronized. Retries receive a fresh signature and timestamp for that delivery attempt.
Canonical message:
<timestamp>.<raw_request_body>
Secret format:
- Webhook secret is provided as a base64 string.
- Base64-decode it before computing HMAC.
Python example:
import base64
import hmac
import hashlib
import re
import time
def verify_webhook(signature_header: str, raw_body: bytes, secret_b64: str, max_age_seconds: int = 600) -> bool:
parts = {}
for part in signature_header.split(","):
if "=" not in part:
return False
k, v = part.strip().split("=", 1)
parts[k.strip()] = v.strip()
if parts.get("v") != "1":
return False
if parts.get("alg") != "hmac-sha256":
return False
try:
ts = int(parts["t"])
supplied = parts["s"]
except (KeyError, ValueError):
return False
if not re.fullmatch(r"[0-9a-f]{64}", supplied):
return False
if max_age_seconds is not None and abs(int(time.time()) - ts) > max_age_seconds:
return False
try:
secret = base64.b64decode(secret_b64, validate=True)
except Exception:
return False
message = str(ts).encode("ascii") + b"." + raw_body
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, supplied)
The most common verification failures are using the base64 secret text directly as the key, signing parsed or reformatted JSON instead of the raw body, including header whitespace in the signed message, or comparing against a timestamp from an earlier retry.
Processing checklist
- Verify
X-Webhook-Signatureagainst raw request body. - Deduplicate by
Idempotency-Key. - Persist or enqueue the verified delivery durably before acknowledging it.
- Return a 2xx response promptly after durable acceptance, then apply the business update idempotently.