QR scan events
Use the qr_code.scanned event when your integration needs to react after a recipient scans a Smart QR code generated by Scribeless.
qr_code.scanned
The event is emitted after Scribeless records the scan. The person scanning the QR code is still redirected to the QR destination even if webhook delivery fails.
Create a subscription first with POST /api/webhooks.
Delivery request
Scribeless sends a POST request to your subscription URL.
Content-Type: application/json
X-Scribeless-Event: qr_code.scanned
X-Scribeless-Delivery: DELIVERY_ID
X-Scribeless-Timestamp: UNIX_TIMESTAMP
X-Scribeless-Signature: sha256=SIGNATURE
Respond with a 2xx status within 5 seconds after accepting the event. Non-2xx responses and timeouts are recorded as failed deliveries.
Payload
{
"id": "evt_qr_scan_SCAN_ID",
"type": "qr_code.scanned",
"created_at": "2026-01-01T10:00:00.000000+00:00",
"team_id": "TEAM_ID",
"scan": {
"id": "SCAN_ID",
"smart_qr_code_id": "SMART_QR_CODE_ID",
"team_id": "TEAM_ID",
"created_at": "2026-01-01T10:00:00.000000+00:00"
},
"qr_code": {
"id": "SMART_QR_CODE_ID",
"key": "abc123",
"destination_url": "https://example.com/offers/SPRING-25",
"recipient_id": "RECIPIENT_ID",
"template_id": "TEMPLATE_ID",
"block_id": "spring-offer",
"size": "24mm",
"options": {
"ecl": "Q",
"dotsType": "square",
"dotsColor": "#000000"
},
"team_id": "TEAM_ID",
"created_at": "2026-01-01T09:00:00.000000+00:00",
"updated_at": "2026-01-01T09:00:00.000000+00:00"
},
"recipient": {
"id": "RECIPIENT_ID",
"first_name": "Ada",
"last_name": "Lovelace",
"campaign_id": "CAMPAIGN_ID",
"status": "pending",
"variables": {
"externalId": "order_12345"
}
}
}
The recipient object contains recipient data available when the scan is recorded. Store only the fields your integration needs.
Use id as the event idempotency key. A delivery can be attempted more than once, so your receiver should ignore events it has already processed.
Verify signatures
Verify webhook signatures with the raw request body, the X-Scribeless-Timestamp header, and the subscription secret.
Scribeless signs:
{timestamp}.{rawBody}
The signature is HMAC-SHA256 and is sent as sha256=<hex digest>.
import crypto from 'node:crypto'
export function verifyScribelessWebhook({ rawBody, timestamp, signature, secret }) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
if (signature.length !== expected.length) {
return false
}
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
)
}
Use the raw body exactly as received. Do not stringify a parsed JSON object before verifying.
Status codes
| Status | Meaning |
|---|---|
2xx | Delivery accepted by your endpoint. |
Non-2xx | Delivery recorded as failed. |
| Timeout | Delivery recorded as failed if your endpoint does not respond within 5 seconds. |