Skip to content

Webhook / Callback Best Practices


Respond Promptly

The gateway expects a 2xx response within 15 seconds. If your endpoint is slow or unreachable, the gateway records the delivery as failed and moves on — there is no automatic retry.

Why: Each callback is delivered with at most one attempt, and deliveries may be skipped entirely while the per-merchant circuit breaker is open. A slow or failing endpoint loses that delivery and you will have to reconcile via the status endpoint. Sustained failures also feed the circuit breaker, which short-circuits further deliveries to your endpoint for a short cooldown window.

Recommended approach:

  • Accept the callback, enqueue it for asynchronous processing, and return 200 OK immediately. A small acknowledgement body such as { "status": "ok" } is fine for your own logging, but the gateway treats any 2xx as successful delivery regardless of body contents.
  • Perform heavy operations (database writes, order fulfilment, notifications) in a background job, not in the HTTP request handler.
import secrets

def callback_handler(request):
    payload = request.json()
    # Validate authenticity first — constant-time compare so the key can't be probed via timing
    presented = request.headers.get("X-API-KEY", "")
    if not secrets.compare_digest(presented, EXPECTED_API_KEY):
        return Response(status=401)
    # Enqueue for async processing — respond within milliseconds
    queue.enqueue("process_payment_callback", payload)
    return Response(json={"status": "ok"}, status=200)

Design Idempotent Callback Handlers

Why: In rare cases (e.g. a gateway process restart between the HTTP send and recording the delivery), the same callback may be delivered more than once. If your handler is not idempotent, you may fulfil an order twice, send duplicate notifications, or credit an account multiple times.

Recommended approach:

  • Use the gatewayReference as a deduplication key.
  • Before processing, check whether you have already processed a callback for this transaction ID.
  • Use database-level uniqueness constraints or an idempotency log table to enforce this.
def process_callback(payload):
    tx_id = payload["gatewayReference"]
    if already_processed(tx_id):
        return  # Skip — already handled
    with db.transaction():
        record_as_processed(tx_id)
        update_order_status(payload)
        # Downstream actions...

Verify Callback Authenticity

Why: Your callback URL is publicly accessible. Without verification, a malicious actor could send forged callbacks to trick your system into marking fraudulent transactions as successful.

Recommended approach:

  • On every incoming callback, verify that the X-API-KEY header matches the API key issued for your brand.
  • Reject any request where the header is missing or does not match.
  • Restrict your callback endpoint to HTTPS only.

See Authentication & Security — Validate Callback Authenticity for the full implementation example.