Webhooks
Last updated: 2026-09-26
Webhooks notify your system about payment order status updates.
Configuration
Authorized merchant users configure the webhook URL in the Merchant Portal. Select Live or Sandbox before saving: each realm has its own URL, signing secret, deliveries, and retry state.
Use a public HTTPS endpoint. After a change, verify the saved URL and complete an end-to-end sandbox check before relying on the same receiver design for live traffic.
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 within an absolute seven-day delivery window. - A valid
Retry-Aftervalue on a retryable response can extend the delay. Both delta-seconds and HTTP-date formats are accepted; values above one hour are treated as one hour. The normal retry schedule includes jitter, so the actual delay before delivery can be longer than one hour. - 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}).
The payload distinguishes requested amount from recognized actualAmount.
For completed pay-ins, customer-credit idempotency is per payment ID; delivery
deduplication does not replace it. Follow the payment reconciliation rules.
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.
For mobile-money-transfer pay-ins, a PENDING -> PENDING update can contain
new mobileMoneyTransferDetails, such as a transfer code that was not ready at
creation. Process those updated details even when the status is unchanged.
Deduplicate by the delivery Idempotency-Key; (paymentId, status) would discard
useful instruction updates. Inspect updatedAt and read the payment again to resolve an uncertain
or out-of-order update.
The same payment can also receive a corrected actualAmount, including on
another completed update. Keep the corrected value for reconciliation under
the existing payment ID. A FAILED update of the same payment can arrive before
the corrected completion; read the payment when the sequence is unclear.
Delivery deduplication must not discard a distinct
correction or produce a second full credit. See
Later amount corrections.
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:
- The webhook signing secret is the base64 segment of the selected realm's
secret API key: the value after
sk_test_orsk_prod_. - Base64-decode it before computing HMAC.
- Rotating API credentials changes this value immediately for that realm. Update webhook verification as part of the same cutover.
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)
key = k.strip()
if key in parts:
return False
parts[key] = 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
if not secret:
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.
If the signing secret is missing or empty, reject the webhook and correct your configuration. Do not verify using an empty key. Duplicate signature parameters are rejected by this example so there is one unambiguous value for each part.
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.