Skip to content

Concepts

Webhooks

On every scan we POST a scan.created event to your endpoints, and on every new code a code.created event to the ones that ask for it. Available on Pro and Business. Add endpoints in the dashboard under API; each endpoint gets its own signing secret (whsec_…), shown once when you add it.

The event#

# POST to your endpoint

Headers:
  Content-Type: application/json
  X-Webhook-Event: scan.created
  X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the raw body>

{
  "id": "d3f1…",              // stable per event; dedupe on it
  "event": "scan.created",
  "created_at": "2026-07-19T14:58:09.915Z",
  "data": {
    "qr_id": "4a38…", "qr_name": "Table tents", "slug": "aB3xY9k",
    "destination": "https://example.com/manual",
    "country": "DE", "city": "Berlin",
    "os": "iOS", "device": "Mobile",
    "scanned_at": "2026-07-19T14:58:09.812Z"
  }
}

device is Mobile, Tablet or Desktop; os is iOS, Android, Windows, macOS, Linux or Other. country is a two-letter code. country and city are Unknownwhen the location can't be told.

Scan data contains no personal information: no IP, no exact location, no fingerprint.

code.created#

Sent when a code is created, from the dashboard, the API or a bulk upload. data is the code object (without scan counts). Subscribe to it with "events": ["code.created"], or tick New codes when adding an endpoint in the dashboard.

# code.created

{
  "id": "7c21…", "event": "code.created", "created_at": "2026-10-12T09:30:00.000Z",
  "data": {
    "id": "4a38…", "name": "Spring flyer", "type": "url",
    "destination": "https://example.com/spring", "slug": "aB3xY9k",
    "short_url": "https://qrvora.com/r/aB3xY9k", "status": "active",
    "created_at": "2026-10-12T09:30:00.000Z"
  }
}

Verify the signature#

Compute HMAC-SHA256 of the raw request body with the endpoint's secret and compare against the header. Use a constant-time comparison and the raw bytes, not the re-serialized JSON:

# Node (Express)

import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/hooks/qr", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.get("X-Webhook-Signature") ?? "";
  const expected =
    "sha256=" +
    createHmac("sha256", process.env.HOOK_SECRET).update(req.body).digest("hex");
  const ok =
    sig.length === expected.length &&
    timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const event = JSON.parse(req.body);
  // …handle after responding; dedupe on event.id
  res.status(200).end();
});

Subscribe over the API#

Integrations can add and remove endpoints themselves, the way Zapier does when a Zap is turned on and off. Same rules as the dashboard: Pro and Business, public http(s) endpoints, up to 50 per account. An integration can add "source": "zapier" (or make, n8n, hubspot), and the dashboard then shows the endpoint as added by it.

# Subscribe, list, unsubscribe

POST /api/v1/webhooks
{ "url": "https://example.com/hooks/qr", "events": ["scan.created", "code.created"] }
→ 201 { "data": { "id": "…", "url": "…", "events": ["scan.created"],
                  "active": true, "created_at": "…", "secret": "whsec_…" } }

GET    /api/v1/webhooks        → { "data": [ …, no secrets ] }
GET    /api/v1/webhooks/:id    → { "data": { …one webhook } }
DELETE /api/v1/webhooks/:id    → { "data": { "id": "…", "deleted": true } }

On Starter, POST answers 403 plan_required. The secret is returned once, on creation.

Recent scans#

GET /api/v1/scans?limit=10&qr_id=…returns your newest scans (up to 25), newest first, each in the same shape as an event's data plus an id. Useful as sample data while setting up an integration. qr_id is optional.

Delivery and retries#

  • Answer with a 2xx within 4 seconds; do slow work after responding.
  • An endpoint that answers 410 Gone is switched off and gets no more deliveries; add it again to restart.
  • Failed deliveries retry with backoff. Retries reuse the same event id, so dedupe on it; only the endpoints that failed are retried.
  • The dashboard shows per-endpoint delivery and failure counts.