Skip to main content

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, and 204.
  • Network errors and HTTP 408, 425, 429, and 5xx responses 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-After value 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 4xx responses, are permanent rejections and are not retried. Redirects are not followed.

Headers​

  • Content-Type: application/json
  • Idempotency-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:

PartMeaning
v=1Signature format version
t=<unix_timestamp>Signing time in Unix seconds
alg=hmac-sha256HMAC algorithm
s=<hex_signature>Lowercase hexadecimal SHA-256 HMAC digest

To verify it:

  1. Capture the exact request-body bytes before JSON parsing, whitespace changes, character decoding, or reserialization.
  2. Split the header on commas, then split each part on the first =. Require v=1, alg=hmac-sha256, an integer t, and a 64-character hexadecimal s.
  3. Base64-decode the configured webhook secret once. The decoded bytes are the HMAC key; the base64 text itself is not the key.
  4. Build the signed bytes as the ASCII timestamp, one literal dot, and the exact raw request body.
  5. Compute HMAC-SHA256, encode the result as lowercase hex, and compare it with s using a constant-time comparison.
  6. 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_ or sk_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​

  1. Verify X-Webhook-Signature against raw request body.
  2. Deduplicate by Idempotency-Key.
  3. Persist or enqueue the verified delivery durably before acknowledging it.
  4. Return a 2xx response promptly after durable acceptance, then apply the business update idempotently.