Scribeless
Webhooks

QR scan events

Receive signed webhook events when Smart QR codes are scanned.

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

StatusMeaning
2xxDelivery accepted by your endpoint.
Non-2xxDelivery recorded as failed.
TimeoutDelivery recorded as failed if your endpoint does not respond within 5 seconds.
Copyright © 2026