REST API & webhooks
Create, change and read your dynamic QR codes from your own software, and get a signed webhook when something is scanned or a lead comes in.
Base URL: https://qrflick.com/api/v1
Authentication
Every request carries an API key in the Authorization header. Keys belong to an account (or team workspace) and are created in Settings → Developers by its owner or an admin.
curl https://qrflick.com/api/v1/me \
-H "Authorization: Bearer api_…"
- Read-only keys can list and fetch codes, cards, leads, scans and images.
- Read & write keys can also create, change and delete codes and webhook endpoints.
- A key stops working if the person who made it leaves the team or stops being an admin, or if the account leaves a plan with API access.
Create a key in Settings → Developers.
Requests, errors and limits
Send and receive JSON. Dates are ISO 8601 in UTC. Ids are the 12-character ids you see in dashboard URLs.
Lists are paginated: use ?page= and ?per_page= (up to 100). Responses carry data, links and meta. Each key has a per-minute request limit; the X-RateLimit-Remaining header shows what’s left.
| Status | Meaning |
|---|---|
401 | Missing, wrong or revoked key. |
403 | The key is read-only, the plan has no API access, or the action isn’t allowed. |
404 | No such record in this account. |
422 | Something in the request is invalid or over a plan limit; errors says which field. |
429 | Too many requests. Wait for the seconds in Retry-After. |
{
"message": "The name field is required.",
"errors": { "name": ["The name field is required."] }
}
Endpoints
| Request | What it does |
|---|---|
GET /me |
The account, plan, key and limits. Handy to test a connection. |
GET /qr-codes |
Your dynamic codes, newest first. Filters: search, type, archived. |
POST /qr-codes |
Create a code: type, name, payload, and optionally design. (needs read & write) |
GET /qr-codes/{id} |
One code, with its short link, destination and scan count. |
PATCH /qr-codes/{id} |
Change any of name, payload, type, design, or paused (true/false). The printed code stays the same. (needs read & write) |
DELETE /qr-codes/{id} |
Delete a code. Its short link then shows “not found”. (needs read & write) |
GET /qr-codes/{id}/image |
The code as png, svg, pdf or eps: ?format=, and ?size= (pixels) or ?mm= and ?dpi=. |
GET /qr-codes/{id}/scans |
Scans per day and breakdowns (country, city, device, os, browser, language, referrer): ?from= and ?to= (YYYY-MM-DD). |
GET /cards |
Your digital business cards. |
GET /cards/{id} |
One card, with its page and short link. |
GET /cards/{id}/leads |
Contacts shared on a card, newest first. ?since= returns only newer ones. |
GET /webhooks |
Your webhook endpoints. |
POST /webhooks |
Subscribe a URL to events (url, events). The reply includes the signing secret. (needs read & write) |
DELETE /webhooks/{id} |
Unsubscribe an endpoint. (needs read & write) |
Example: create and change a code
Create a website code. The reply has the short link to print or share.
curl -X POST https://qrflick.com/api/v1/qr-codes \
-H "Authorization: Bearer api_…" \
-H "Content-Type: application/json" \
-d '{"type": "url", "name": "Summer menu", "payload": {"url": "https://example.com/menu"}}'
{
"data": {
"id": "k3v9x2m7qa1b",
"object": "qr_code",
"name": "Summer menu",
"type": "url",
"payload": { "url": "https://example.com/menu" },
"short_url": "https://qrflick.com/Ab3xY9k",
"domain": "qrflick.com",
"destination": "https://example.com/menu",
"status": "active",
"scans": 0,
"last_scanned_at": null,
"archived": false,
"created_at": "2026-01-31T10:00:00+00:00",
"updated_at": "2026-01-31T10:00:00+00:00"
}
}
Optional "domain": the host of a platform redirect domain your workspace may use (as offered when creating a code in the app). Leave it out for the standard short domain. The address can't be changed through the API once created.
Later, point the same printed code somewhere else:
curl -X PATCH https://qrflick.com/api/v1/qr-codes/k3v9x2m7qa1b \
-H "Authorization: Bearer api_…" \
-H "Content-Type: application/json" \
-d '{"payload": {"url": "https://example.com/autumn-menu"}}'
Download it as a print-ready SVG, 40 mm wide:
curl "https://qrflick.com/api/v1/qr-codes/k3v9x2m7qa1b/image?format=svg&mm=40" \
-H "Authorization: Bearer api_…" -o menu.svg
QR code types and payload fields
The payload holds the fields for the code’s type. Business card and profile codes are made with their card or profile, not here.
| type | Payload fields |
|---|---|
url |
url*
|
whatsapp |
phone*,
message
|
phone |
phone*
|
email |
email*,
subject,
body
|
sms |
phone*,
message
|
maps |
query*
|
google_review |
place_id*
|
link_bio Pro |
title*,
description,
links*
|
menu Pro |
title*,
description,
menu_url*
|
app_store Pro |
ios_url*,
android_url*,
fallback_url*
|
event Pro |
title*,
description,
starts_at*,
location,
url
|
coupon Pro |
title*,
description,
code*,
expires_at,
url
|
social |
platform*,
handle*
|
file |
title*,
description,
file_url
|
* required. Pro types need a plan with smart redirects.
Webhooks
Add an endpoint in Settings → Developers (or with POST /webhooks) and pick its events. Each event is POSTed as JSON:
| Event | Sent when |
|---|---|
qr_code.created | A QR code was created. |
qr_code.updated | A QR code got a new destination, status or setting. |
qr_code.deleted | A QR code was deleted. |
scan.created | Someone scanned a code (bots and link previews are left out). |
lead.created | Someone shared their contact on a card or wrote through a profile. |
POST /your/endpoint
Content-Type: application/json
webhook-id: evt_01jbx6q9d2m8w4k7r3t5y0a1bc
webhook-timestamp: 1767225600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "evt_01jbx6q9d2m8w4k7r3t5y0a1bc",
"type": "scan.created",
"created_at": "2026-01-01T00:00:00+00:00",
"data": {
"qr_code": { "id": "k3v9x2m7qa1b", "name": "Summer menu", "type": "url" },
"short_url": "https://qrflick.com/Ab3xY9k",
"scanned_at": "2026-01-01T00:00:00+00:00",
"country": "ES", "city": "Madrid", "device": "mobile", "os": "ios",
"browser": "safari", "language": "es", "referrer": null, "variant": null
}
}
- Reply with any 2xx status within 10 seconds; do slow work after replying.
- Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h.
- After 20 events in a row fail every try, the endpoint is paused and the owner gets an email.
- Retries and “Send again” keep the same webhook-id: use it to skip duplicates.
- Scans never include IP addresses. Lead events include the contact details the visitor chose to share.
Checking signatures
Deliveries are signed the Standard Webhooks way. Sign “webhook-id.webhook-timestamp.raw body” with HMAC-SHA256, using the base64-decoded part of your secret after whsec_, and compare with the webhook-signature header. Reject old timestamps to stop replays.
// PHP
$secret = 'whsec_…';
$payload = $id . '.' . $timestamp . '.' . $rawBody;
$key = base64_decode(substr($secret, 6));
$expected = 'v1,' . base64_encode(hash_hmac('sha256', $payload, $key, true));
$valid = abs(time() - (int) $timestamp) < 300
&& hash_equals($expected, $signatureHeader);
// Node.js
import crypto from 'node:crypto';
const key = Buffer.from(secret.slice(6), 'base64');
const expected = 'v1,' + crypto.createHmac('sha256', key)
.update(`${id}.${timestamp}.${rawBody}`).digest('base64');
const valid = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
Standard Webhooks libraries (standardwebhooks.com) for most languages check this for you.