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.

Webhook setup

This guide walks through everything you need to receive and handle webhook events reliably.

Step 1 — Create an endpoint

Create a webhook endpoint via the API:

Terminal window
curl -X POST https://api.qairopay.com/v1/webhook-endpoints \
-H "Authorization: Bearer $QAIROPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/qairopay",
"enabled_events": ["pass.installed", "pass.revoked", "payment.authorized"]
}'

Or via the dashboard: Developer → Webhooks → Add endpoint.

The response includes a signing_secret — this is the only time it’s shown in plaintext. Copy it to a secure secrets manager before closing the response.

Step 2 — Receive your first delivery

Every delivery shares the same envelope shape:

{
"id": "wevt_01HXYZ",
"type": "pass.installed",
"created": 1716144300,
"api_version": "2026-04-01",
"data": {
"pass": { "id": "pss_01HABC", "status": "installed", "..." }
}
}

Key fields:

  • id — unique event ID, use it to deduplicate retries
  • type — the event name; switch on this to route handling
  • created — Unix timestamp (seconds) when the event was emitted
  • api_version — the version this payload conforms to
  • data — event-specific payload; shape matches the resource’s GET response

Step 3 — Verify the signature

Always verify the QairoPay-Signature header before processing a payload. Use the raw request body bytes — do not parse JSON first.

The signing algorithm: message = <unix_timestamp>.<raw_body_bytes>, HMAC-SHA256, header format = t=<unix>,v1=<hex>.

TypeScript:

import { QairoPay } from "@qairopay/sdk";
// In an Express handler:
app.post("/webhooks/qairopay", express.raw({ type: "application/json" }), async (req, res) => {
const event = await QairoPay.webhooks.constructEvent(
req.body, // raw Buffer — NOT req.body parsed as JSON
req.headers['QairoPay-Signature'],
process.env.QAIROPAY_WEBHOOK_SECRET!,
);
// handle event.type ...
res.sendStatus(200);
});

Python:

import os
from qairopay import webhooks, WebhookSignatureError
def handle_webhook(raw_body: bytes, sig_header: str) -> tuple[int, str]:
try:
event = webhooks.construct_event(
raw_body, # raw bytes — not a parsed dict
sig_header,
os.environ["QAIROPAY_WEBHOOK_SECRET"],
tolerance=300,
)
except WebhookSignatureError as err:
return 400, f"Signature failed: {err.reason}"
# handle event.type ...
return 200, "ok"

Default tolerance is 300 seconds (5 minutes). Events older than the tolerance window are rejected to prevent replay attacks.

Step 4 — Handle retries

If your endpoint returns a non-2xx status or times out, QairoPay retries on a fixed schedule:

AttemptDelay from event emission
1Immediate
25 minutes
330 minutes
42 hours
55 hours

See Retries and delivery for the full reference including what HTTP statuses trigger retries.

Design for at-least-once delivery. Acknowledge with 200 OK as soon as you’ve durably enqueued the event. Deduplicate by event.id in your processing layer.

Step 5 — Rotate your signing secret

Rotate the signing secret periodically or when it may have been exposed.

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

The response includes the new signing_secret. During rotation, QairoPay sends both the old and new signatures in the QairoPay-Signature header for a 10-minute overlap window, so you can update your secret without dropping deliveries:

QairoPay-Signature: t=1716144300,v1=<new_sig>,v1=<old_sig>

The SDK’s constructEvent (and construct_event in Python) automatically tries each v1= value in order. No code changes needed during the rotation window — just update your QAIROPAY_WEBHOOK_SECRET environment variable and redeploy within 10 minutes.

Step 6 — Recover from auto-disable

If all 5 delivery attempts fail, the endpoint is automatically disabled:

  • status"disabled", disabled_reason"retry_exhausted"
  • A webhook_endpoint.disabled event is emitted to all other active endpoints

After fixing the underlying connectivity issue, re-enable the endpoint:

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"}'

Events delivered while the endpoint was disabled are not automatically replayed. You can replay individual delivery attempts from the dashboard or via the API: POST /v1/webhook-endpoints/:id/delivery-attempts/:aId/retry.