Skip to main content

Webhooks

Last updated: 2026-05-22

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
  • Delivery is considered successful only on HTTP 200
  • Any non-200 response is treated as failure and retried
  • Retries use exponential backoff with jitter until the delay reaches about 1 hour, then continue on roughly hourly intervals with jitter (up to 100 attempts)

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}).

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 late provider confirmation can update FAILED to COMPLETED. Use Idempotency-Key to deduplicate deliveries safely and use the latest accepted paymentOrder.status for reconciliation.

Signature verification

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 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(","):
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"])
except Exception:
return False

if max_age_seconds is not None and abs(int(time.time()) - ts) > max_age_seconds:
return False

secret = base64.b64decode(secret_b64)
message = str(ts).encode("utf-8") + b"." + raw_body
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()

return hmac.compare_digest(expected, parts.get("s", ""))

Processing checklist

  1. Verify X-Webhook-Signature against raw request body.
  2. Deduplicate by Idempotency-Key.
  3. Process asynchronously in your queue/worker.
  4. Return HTTP 200 quickly after enqueue.