Developers
API reference (v1)
Authenticate with an API key, manage QR codes and pages, download scans and images, and receive webhooks.
The qraffic REST API lets you create and read QR codes, download scan data, manage pages, and receive webhooks. Send your API key as a Bearer token to https://qraffic.com/api/v1. API keys are included from the Pro plan upward, see plans and billing. A machine-readable OpenAPI description is published at /api/v1/openapi.json.
How do I get an API key?
Open Settings, then API Keys, and create a key with a name of up to 100 characters. Admins and owners can create and revoke keys. The full key is shown once, so store it safely. It starts with sk_live_.
How do I authenticate?
curl https://qraffic.com/api/v1/me \
-H "Authorization: Bearer $QRAFFIC_API_KEY"
Every response is JSON in one of two shapes: { "ok": true, "data": ... } or { "ok": false, "error": "..." }. A missing header returns 401 with Missing Authorization: Bearer <key> header., and a wrong or revoked key returns 401 with "Invalid or revoked API key."
What are the rate limits?
200 requests per minute per key. Over the limit you get a 429 with "Rate limit exceeded. Please retry shortly." Wait a moment and retry.
What can I do with QR codes?
| Request | What it does |
| --- | --- |
| GET /api/v1/me | Returns your account_id and key_id |
| GET /api/v1/qr | Lists codes. limit defaults to 100, maximum 500. Dynamic codes include short_url |
| POST /api/v1/qr | Creates a code. Returns 201 with id, slug, and short_url |
| GET /api/v1/qr/{id} | Returns one code |
| GET /api/v1/qr/{id}/scans | Returns scan events as JSON or CSV |
| GET /api/v1/qr/{id}/image | Returns the code image as PNG or SVG |
Creating a code takes the same fields as the dashboard: name, kind (dynamic or static), payload_type, destination_url, static_content, payload, routing_mode, rotation, style, status, tags, and slug. The same plan limits apply. A request over your plan or with invalid fields returns 400 with a message.
For scans, use format=json or format=csv, sinceDays to limit the window, and limit (default 1000, maximum 5000). CSV columns are created_at, country, region, city, device_type, os, browser, referrer_host, utm_source, utm_medium, utm_campaign, variant_id, rule_id, destination_url, and is_bot.
For images, use format=png or svg. For PNG, size runs from 64 to 2048 and defaults to 512. The image uses the style saved on the code, and responses can be cached for 300 seconds.
What can I do with pages?
| Request | What it does |
| --- | --- |
| GET /api/v1/pages | Lists pages. Filter with status (draft, published, archived) and limit |
| POST /api/v1/pages | Creates a page from a templateId or a doc |
| GET, PATCH, DELETE /api/v1/pages/{id} | Reads, updates, or deletes a page |
| POST /api/v1/pages/{id}/publish | Publishes a page |
| /api/v1/pages/{id}/recipients | Lists, creates (up to 500 at once), updates, and deletes personalized recipients |
| GET /api/v1/page-templates | Lists templates, with /{id} for one |
Page settings include noindex, ogImageUrl, faviconUrl, and embedOrigins, see landing pages.
How do webhooks work?
Webhooks send an HTTP POST to your URL when something happens. You manage them through the API: GET and POST /api/v1/webhooks (up to 20 endpoints per account), GET, PATCH, DELETE /api/v1/webhooks/{id}, POST /api/v1/webhooks/{id}/rotate-secret, POST /api/v1/webhooks/{id}/test, and GET /api/v1/webhooks/{id}/deliveries. GET /api/v1/webhook-events lists event types.
Create one with url, events (a list of event types, or * for all), and optionally description and active. The signing secret starts with whsec_ and is returned once.
Event types are qr.scanned, page.viewed, page.cta_clicked, page.checkout_handoff, page.form_submitted, form.submitted, and webhook.test. Bot scans and link previews are not sent as qr.scanned.
Each delivery is JSON with id, type, created_at, account_id, and data.
How do I verify a webhook signature?
Each delivery carries an x-qraffic-webhook-signature header of the form t=<unix time>,v1=<hex digest>. The digest is an HMAC-SHA256 of the string <t>.<raw request body> using your secret. Compute it on the raw body, compare it to v1, and reject deliveries whose timestamp is more than 300 seconds old.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return fresh && a.length === b.length && timingSafeEqual(a, b);
}