Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

Retries and delivery

Webhook delivery is at-least-once with fixed-schedule retries on failure. A successful delivery is any 2xx response within 10 seconds.

Retry timeline at a glance

If your endpoint isn’t reachable, we retry up to 4 more times on a fixed schedule that totals roughly 5 hours:

flowchart LR
A(["Initial<br/>delivery"]) --> B(["Attempt 2<br/>+5 min"])
B --> C(["Attempt 3<br/>+30 min"])
C --> D(["Attempt 4<br/>+2 hr"])
D --> E(["Attempt 5<br/>+5 hr"])
E --> F{{"Endpoint<br/>auto-disabled"}}
classDef stage fill:#E2E6EE,stroke:#2A3A5E,color:#0F172A;
classDef terminal fill:#F8E5E1,stroke:#C64A3B,color:#8B3023;
class A,B,C,D,E stage
class F terminal
Delays are measured from the original emitted_at timestamp, not from the previous attempt. After 5 consecutive failures the endpoint is automatically disabled.

Retry schedule

AttemptDelay from emitted_at
1 (initial)Immediate
25 minutes
330 minutes
42 hours
55 hours

All delays are measured from the original emitted_at timestamp on the event — not from the time of the previous attempt.

What counts as failure

We retry if:

  • The request times out (no response after 10 seconds).
  • We receive a 408, 409, 425, 429, or any 5xx response.
  • The TLS handshake fails.
  • The connection is refused or dropped mid-stream.

We do not retry if:

  • We receive any other 4xx response. We assume your endpoint is rejecting deliberately (signature failure, schema rejection, etc.) and that retrying won’t help.
  • We receive a 2xx response. Even if your handler errored after responding, we consider the delivery successful.

Auto-disable after exhaustion

When attempt 5 fails, the endpoint is automatically disabled:

  • status is set to "disabled"
  • disabled_reason is set to "retry_exhausted"
  • A webhook_endpoint.disabled event is emitted to all other active endpoints on your account

To re-enable the endpoint after fixing the underlying issue:

Terminal window
curl -X PATCH https://api.qairopay.com/v1/webhook-endpoints/whe_01HABC \
-H "Authorization: Bearer $QAIROPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "enabled"}'

You can also re-enable from the Webhooks section of the dashboard.

Manual retry

Any individual delivery attempt can be retried from the dashboard or via the API, regardless of whether the endpoint is currently enabled:

Terminal window
curl -X POST https://api.qairopay.com/v1/webhook-endpoints/whe_01HABC/delivery-attempts/wdla_01HXYZ/retry \
-H "Authorization: Bearer $QAIROPAY_API_KEY"

This resets that attempt’s status and immediately queues a new delivery. Manual retries do not count toward the 5-attempt auto-disable threshold.

SSRF policy

To prevent server-side request forgery, QairoPay enforces the following on webhook endpoint URLs:

  • URLs must use HTTPS in the live environment (https://).
  • The following destination addresses are blocked:
    • RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    • IPv4/IPv6 loopback (127.0.0.0/8, ::1)
    • Link-local (169.254.0.0/16, including the GCP metadata server at 169.254.169.254)
    • Any DNS name that resolves to the above ranges

Endpoints that fail the SSRF check are rejected at creation time with a 422 Unprocessable Entity response.

HTTP endpoints (http://) are allowed in the sandbox environment only and are blocked in live.

Designing your endpoint for at-least-once delivery

Two requirements your handler should satisfy:

  1. Acknowledge fast, process async. Reply 200 OK as soon as you’ve durably enqueued the event for processing (e.g., wrote it to a queue). The 10-second budget is generous, but you should not spend it doing CPU-bound work or database writes that might block.
  2. Deduplicate by event.id. The same event id can arrive twice (rarely, but it happens). Persist the id with a unique constraint, or use an idempotent upsert keyed by id.

A canonical pattern:

export async function handler(req: Request) {
const event = verifyAndParse(req); // throws on invalid signature
await db.webhookEvents.insertIgnoreConflict({
id: event.id,
received_at: new Date(),
payload: event,
});
await queue.enqueue("process-event", { id: event.id });
return new Response("ok");
}

The downstream worker reads from the queue, fetches the event by id, processes it, and marks done. Re-deliveries hit the insertIgnoreConflict and short-circuit harmlessly.

Event ordering

Events for different resources can arrive in any order. Events for the same resource are best-effort ordered by created time, but at-least-once delivery means you may occasionally see a pass.updated arrive before the pass.created it depends on (if the latter was retried).

Resolve this by treating every event as a state update on a snapshot, using previous_attributes to detect ordering anomalies, and refetching the resource from the API when in doubt. Don’t rely on event order for correctness.

Scaling your endpoint

If you’re processing high-volume events (e.g., card.transaction.authorized at peak hours):

  • Use a queue between receipt and processing — at-least-once delivery from QairoPay plus retries inside your system gives you a clean separation.
  • Run the receipt handler in an autoscaling environment (serverless or a long-lived service with HPA).
  • Watch your endpoint’s p99 response time in the QairoPay dashboard. If it creeps toward 10 s, you’re heading for retry storms.