Webhooks
Get a signed POST for every scan or lead, or when a code reaches a scan count.
Webhooks are on Pro and Business. Owners and admins can add them. They work with Make, n8n, Zapier and your own servers.
Add a webhook
- Open Integrations and find Webhooks.
- Enter the Endpoint URL. It must be a public
https://address on the standard port, with a hostname rather than an IP address. - Tick one or more Events:
scan.created: every human scan.lead.created: every new lead.scan.threshold: once, when the workspace's total human scans reach the Scan threshold you set.
- Click Add webhook.
- Copy the Signing secret (it starts with
whsec_). It's shown only once.
Up to 10 webhooks per workspace. To change one, delete it and add it again.
The request
We send a POST with a JSON body:
{
"event": "scan.created",
"data": { ... }
}
Headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | DynaQR-Webhooks/1 |
x-qrm-event | The event name |
x-qrm-signature | t=<unix seconds>,v1=<signature> |
scan.created data
{
"id": "6f1c…",
"codeId": "0b9e…",
"organizationId": "…",
"hostname": "qrmgr.link",
"ts": "2026-11-14T15:04:05.000Z",
"source": "qr",
"country": "US",
"region": "TX",
"city": "Austin",
"device": "ios",
"os": "iOS",
"browser": "Safari",
"language": "EN-US",
"ruleId": null,
"isBot": false
}
source is qr or link. device is ios, android, desktop or other. Location fields can be null.
lead.created data
{
"id": "…",
"codeId": "…",
"organizationId": "…",
"formId": "…",
"ts": "2026-11-14T15:04:05.000Z",
"values": {
"firstName": "Ana",
"lastName": "Ruiz",
"email": "[email protected]",
"phone": "+15125550123",
"address": { "line1": "…", "city": "Austin", "state": "TX", "postalCode": "78701" },
"message": "Quote for a new hot water cylinder, please."
},
"consent": {
"terms": { "shown": true, "text": "I agree to the Terms and Conditions", "checked": true, "url": "https://example.com/terms" },
"marketing": { "shown": true, "text": "I'd like to receive news and offers from Joe's Pizza", "checked": false },
"at": "2026-11-14T15:04:05.000Z"
},
"country": "US",
"region": "TX",
"city": "Austin",
"device": "ios",
"utm": { "source": "newsletter" }
}
Fields the form doesn't collect are left out. message is the optional Message field, up to 2,000 characters of plain text typed by the visitor, so treat it as untrusted input. utm holds any UTM parameters on the link the visitor arrived from, without the utm_ prefix. terms and marketing are null when those checkboxes are off.
scan.threshold data
{ "threshold": 1000, "scans": 1003, "codeId": null }
scans is the total when the threshold was crossed.
Verify the signature
The signature is an HMAC-SHA256 of <t>.<raw body>, keyed with your signing secret, in hex. To verify:
- Split
x-qrm-signatureintotandv1. - Compute HMAC-SHA256 of the string
t + "." + rawBodywith your secret. - Compare it to
v1using a constant-time comparison. - Reject requests where
tis more than a few minutes old, to stop replays.
Use the raw request body, before any JSON parsing. Node.js example:
import crypto from 'node:crypto';
export function verifyQrmWebhook({ rawBody, header, secret }) {
const parts = Object.fromEntries(
header.split(',').map((part) => part.split('=')),
);
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Delivery and retries
- Respond with any
2xxstatus within 5 seconds. Redirects aren't followed. - Any other response, or a timeout, is retried with growing delays (2, 4, 8 minutes and so on), up to 8 attempts over about 4 hours. Each attempt is signed with a new timestamp.
- Respond
410 Goneto stop a delivery without retries. - A delivery can arrive more than once. Use
data.id(scans and leads) to ignore duplicates.
Recent deliveries, under the webhooks list, shows the last 50 deliveries with their status, attempts and last error.