Core resources
Codes
A code is one dynamic QR: a short link plus where it points. The printed image encodes the short link and never changes; everything else stays editable.
The code object#
| id | UUID, stable for the code's lifetime |
| name | internal label, never shown to scanners |
| type | url or a page type (links, menu, vcard, …) |
| destination | url codes: the target URL. Page codes: the hosted page path (/p/slug), managed for you |
| routing | url codes only: smart rules that send some scanners to other URLs, or null; see Smart rules |
| slug | 7-character short-link id |
| short_url | the URL the printed QR encodes; never changes |
| status | active or paused; paused codes keep redirecting but stop counting scans |
| scans | all-time scan count, near-real-time |
| created_at | ISO 8601 timestamp |
List codes
GET/api/v1/codes
Your codes, newest first. Query: limit (default 25, max 100; larger values clamp) and offset (default 0). To search, add q (codes whose name contains it, any case) or slug (one short link). total counts the codes that match.
# Page through every code
let offset = 0, total = Infinity;
while (offset < total) {
const res = await fetch(
"https://qrvora.com/api/v1/codes?limit=100&offset=" + offset,
{ headers: { Authorization: "Bearer " + process.env.QRF_KEY } }
);
const page = await res.json();
total = page.total;
offset += page.data.length;
// …use page.data; stay under 6 requests/second
}Get a code
GET/api/v1/codes/:id
One code including its all-time scans count. 404 not_found for ids that are missing or belong to another account.
Create a code
POST/api/v1/codes
Creates one dynamic code and returns it with 201. Enforces your plan's code-count limit first (403 code_limit_reached).
| type | optional, default url. One of: url, links, menu, vcard, video, coupon, apps, event, business, feedback, social, gallery, audio, tag. PDF codes are dashboard-only (422 unsupported_type); an audio code starts without tracks, which you upload in the dashboard |
| name | optional, max 120 characters, default API QR code |
| destination | url codes only: an http(s) URL, checked against our malware/phishing list (422 unsafe_destination) |
| content | page codes only: the page payload for the type; see Page content. Omit it to create an empty page and fill it later |
| routing | url codes only, optional: smart rules; see Smart rules. Pro and Business plans (403 routing_not_available) |
# Create a menu page code
curl -X POST https://qrvora.com/api/v1/codes \
-H "Authorization: Bearer qrf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "menu", "name": "Cafe menu",
"content": {
"title": "Cafe Aurora", "currency": "$",
"sections": [{ "name": "Coffee", "items": [
{ "name": "Espresso", "price": "3.50", "desc": "Double shot" }
]}]
}
}'Bulk create
POST/api/v1/codes/bulk
Up to 100 codes in one call. Body: { "codes": [ { type?, name?, destination?, content? }, … ] }. Partial success is normal: valid specs are created, invalid ones and specs past your plan limit come back in errors with their array index. Always check errors.
# Response shape
HTTP 201 when at least one code was created, 422 when none were
{
"created": [ { …code }, … ],
"errors": [ { "index": 1, "error": "invalid_type", "message": "…" } ],
"summary": { "requested": 3, "created": 2, "failed": 1 }
}Update a code
PATCH/api/v1/codes/:id
Updates any subset of the fields below; send at least one (422 empty_patch otherwise). The printed QR keeps working through every edit.
| name | max 120 characters; longer values are truncated |
| destination | url codes only. Page codes return 422 destination_not_editable; edit their content instead. Same URL validation as create |
| status | active or paused |
| content | page codes only (url and pdf return 422 content_not_editable). Replaces the page payload; see Page content |
| routing | url codes only. Replaces the whole rule set; null or an empty rules list removes it. See Smart rules |
Smart rules#
A url code can carry up to 10 rules that send some scanners to a different URL than its destination: by device, country, phone language, time of day or date. Rules are tried in order and the first match wins; a scanner no rule matches goes to destination. Available on the Pro and Business plans. After a downgrade the rules stay stored but stop applying.
| timezone | IANA zone the time and dates rules are read in, e.g. Europe/London. Default UTC |
| rules[].url | where a matching scanner goes; same validation as destination |
| rules[].enabled | optional, default true; false keeps the rule but skips it |
| rules[].when | one condition, chosen by kind: device, country, language, time or dates |
| kind: device | values: any of ios, android, desktop |
| kind: country | values: ISO 3166-1 alpha-2 codes (US, GB, DE), up to 30 |
| kind: language | values: language codes (en, es, fr), up to 30. Matches the first language the phone asks for |
| kind: time | from and to as HH:MM (24-hour); days as numbers 0 (Sunday) to 6, empty for every day. to earlier than from runs past midnight |
| kind: dates | from and until as YYYY-MM-DD, inclusive; either may be an empty string |
# App download code: stores by device, website for everyone else
curl -X PATCH https://qrvora.com/api/v1/codes/CODE_ID \
-H "Authorization: Bearer qrf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "https://example.com/app",
"routing": {
"timezone": "Europe/London",
"rules": [
{ "when": { "kind": "device", "values": ["ios"] },
"url": "https://apps.apple.com/app/id000000000" },
{ "when": { "kind": "device", "values": ["android"] },
"url": "https://play.google.com/store/apps/details?id=com.example.app" },
{ "when": { "kind": "time", "days": [1,2,3,4,5], "from": "09:00", "to": "17:00" },
"url": "https://example.com/open-now" }
]
}
}'Each rule comes back with an id. A rule that is malformed returns 422 invalid_routing with a message naming the problem, and every rule URL is checked against the malware/phishing list (422 unsafe_destination).
Delete a code
DELETE/api/v1/codes/:id
Deletes the code and its uploaded files. Returns { "deleted": true, "id" }. The short link stops working immediately and the slug is not reused. This cannot be undone.
Render the image
GET/api/v1/codes/:id/image
Renders the code's QR with its saved style. The image encodes the short link, so editing the destination doesn't change the image. Responds with the binary image (image/png or image/svg+xml), not JSON. Each render counts against your quota; cache the result.
| format | png (default) or svg |
| size | PNG pixel width, default 512, clamped to 128-2000 |
curl -o code.png "https://qrvora.com/api/v1/codes/CODE_ID/image?size=1024" \ -H "Authorization: Bearer qrf_YOUR_KEY"
Scan stats
GET/api/v1/codes/:id/stats
A code's scans over a period: the total, each day, and the top 10 countries, cities, devices and systems. Query: from and to as YYYY-MM-DD (default: the last 30 days; at most 366 days apart, otherwise 422 invalid_range).
# Response
{ "data": {
"qr_id": "4a38…", "name": "Menu", "from": "2026-10-01", "to": "2026-10-07",
"total": 8,
"days": [ { "day": "2026-10-01", "count": 7 }, { "day": "2026-10-03", "count": 1 } ],
"countries": [ { "name": "US", "count": 7 }, … ],
"cities": [ … ], "devices": [ … ], "os": [ … ]
} }Safe retries#
Send an Idempotency-Key header (any unique string up to 255 characters) with POST /api/v1/codes or /bulk. If the request is sent again with the same key within 24 hours, you get the first answer back, marked Idempotent-Replayed: true, instead of a second code. A request that failed doesn't keep its key, so a retry runs again. While the first is still running, a repeat gets 409 request_in_progress; a key reused on another endpoint gets 422 idempotency_key_reused.
Building a page code? The payload shapes live in Page content.