Webhooks
ExaRoutes can deliver a webhook for QR scans, form submissions, lifecycle changes, and billing events. Use webhooks to forward events to your own systems (CRM, data warehouse, Slack notifier, etc.).
Configuring a webhook
From the dashboard at Settings → Integrations, add a webhook URL. Each webhook is workspace-scoped and can be subscribed to specific events:
scan.created: every dynamic QR scanform.submitted: every hosted-form submissionqr.created,qr.updated,qr.deleteddomain.verifiedserialized.first_scanned: the first (genuine) scan of a serialized codeserialized.duplicate_scanned: a scan of a code that was already claimed (possible counterfeit)subscription.activated,subscription.renewed,subscription.cancelled,subscription.payment_failed,subscription.plan_changed,subscription.refunded
Delivery envelope
Every delivery is a POST with this JSON envelope; data varies per event:
{
"id": "evt_abc123",
"type": "scan.created",
"createdAt": "2026-04-25T15:00:00Z",
"data": { }
}Event payloads (data)
scan.created
{
"qrId": "qr_abc123",
"shortId": "ABC123",
"country": "US",
"device": "mobile",
"routedTo": "https://yourbrand.com/us",
"variantId": "a",
"variantName": "Landing A"
}routedTo is present only when a routing rule fired; variantId /
variantName only when an A/B campaign picked a variant. The scanner’s IP
and user agent are never sent.
form.submitted
{ "formId": "form_abc123", "formName": "Event RSVP", "submissionId": "sub_9y8x7w" }Fetch the submission values via
GET /api/qr/forms/{formId}/submissions;
they are not included in the webhook.
qr.created
{ "qrId": "qr_abc123", "shortId": "ABC123", "dataType": "url", "secure": [], "subscriptionId": "sub_..." }qr.updated
{ "qrId": "qr_abc123", "shortId": "ABC123", "subscriptionId": "sub_...", "changedKeys": ["data", "style"] }qr.deleted
{ "qrId": "qr_abc123", "shortId": "ABC123", "subscriptionId": "sub_..." }domain.verified
{ "domain": "qr.yourbrand.com" }Serialized scans
Both serialized events share one data shape; serialized.duplicate_scanned
is the possible-counterfeit signal (the code had already been claimed):
{
"itemId": "a1b2c3d4e5f6a7b8",
"batchId": "batch_9y8x7w6v5u",
"productSku": "ACME-1234",
"productName": "ACME Premium Widget",
"scanCount": 2,
"firstScannedAt": 1717100000000,
"country": "US"
}firstScannedAt is when the code was originally claimed (epoch ms);
country is the coarse country of this scan.
subscription.*
Billing lifecycle events carry the subscriptionId plus the provider
metadata for the change (plan, status, billing period). Treat the shape as
informational and re-fetch the subscription from the API for decisions.
Signing
Deliveries are signed with the Standard Webhooks scheme. Each request carries:
webhook-id: the delivery idwebhook-timestamp: Unix secondswebhook-signature:v1,<base64 HMAC-SHA256>computed over"{webhook-id}.{webhook-timestamp}.{raw body}"with your webhook secret
Verify with any Standard Webhooks library, and reject deliveries whose
timestamp is too old (replay protection). The legacy
X-ExaRoutes-Signature: sha256=<hex> header (HMAC of the body alone) is
still sent for backward compatibility, but new integrations should verify
the Standard Webhooks headers.
Retries
Failed deliveries (non-2xx response or timeout) are retried with exponential backoff up to 5 attempts: 30s, 5m, 30m, 2h, 6h. After the final attempt, the delivery is marked failed and visible in the dashboard’s webhook delivery log.