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:
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 retriestype— the event name; switch on this to route handlingcreated— Unix timestamp (seconds) when the event was emittedapi_version— the version this payload conforms todata— event-specific payload; shape matches the resource’sGETresponse
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 osfrom 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:
| Attempt | Delay from event emission |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 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.
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.disabledevent is emitted to all other active endpoints
After fixing the underlying connectivity issue, re-enable the endpoint:
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.