Developer API
Create codes, change where they go and read their scans from your own software. JSON in, JSON out.
Authentication
Create a key in Settings → API keys, then send it with every request. Keys belong to a workspace, so anything a key creates shows up in that workspace’s dashboard.
Authorization: Bearer qrt_live_…When you create a key, pick what it can do. A full access key can do everything below. A read only key can list and fetch codes, their stats and their images, but any POST, PATCH or DELETE with it returns 403 read_only_key and changes nothing. Read-only keys suit dashboards and reports. Only the workspace owner and admins can create or revoke keys.
A key stops working if the person who made it leaves the workspace, is removed, or is made a member. For software that runs for a long time, have the owner make it.
The Free plan allows 60 requests per minute for the whole workspace, shared by all its keys. Every response includes x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (when the minute resets, in Unix seconds). Going over returns 429 rate_limited with a retry-after header. Because the limit is shared, a busy key can use up the minute for every other key in the workspace.
Errors
Errors use normal HTTP status codes and a consistent body:
{
"error": {
"code": "destination_rejected",
"message": "Link shorteners and trackers can't be used as destinations."
}
}400 invalid_json: the body isn’t valid JSON, or isn’t a JSON object400 offset_too_large: offset is over 10,000; page with cursors instead401 unauthorized: missing, invalid or revoked key, including a key that was turned off because the person who made it left the workspace, was removed, or was made a member402 plan_limit_reached: you’ve used all your editable codes403 forbidden: the workspace’s plan doesn’t include API access. Every plan includes it today, Free too, so this is only a safeguard403 read_only_key: this key is read only, so it can’t make, change or delete codes; use a full-access key403 workspace_suspended: this workspace has been suspended403 code_disabled: this code was stopped for safety and can’t be changed or deleted404 not_found: no such code in this workspace409 conflict: the code changed while you were updating or deleting it; fetch it again before retrying. A code deleted or moved to another workspace meanwhile answers404 not_foundinstead410 code_deleted: an Idempotency-Key replay of a code that has since been deleted413 too_large: the request body is too big422 destination_rejected,missing_field,invalid_field,invalid_request: fix the request and try again422 cursor_expired: a cursor more than 7 days old; start again from the first page422 invalid_header,idempotency_key_reused: see Idempotency-Key429 rate_limited: the workspace used its requests for this minute (all its keys share one limit), or its searches or statistics for this minute; wait forretry-afterseconds429 stats_busy: the workspace is already working out two sets of statistics; wait forretry-afterseconds500 server_error: something went wrong on our side; retrying later is safe503 unavailable: we couldn’t check your key just now; retry afterretry-afterseconds503 temporarily_unavailable: a change to a code was refused because we couldn’t save it safely just now. Nothing was changed, so the same request is safe to retry in a moment.
The code object
{
"id": "7f1c2a9e-…",
"name": "Spring menu",
"short_code": "Ab3dE7k",
"short_url": "https://qrtoast.com/Ab3dE7k",
"destination_url": "https://mybakery.com/menu",
"status": "active",
"folder": "Menus",
"scan_count": 1284,
"last_scanned_at": "2026-11-09T14:02:11.000Z",
"created_at": "2026-11-01T09:00:00.000Z",
"updated_at": "2026-11-08T17:30:00.000Z"
}status is active, paused or disabled (stopped for safety by our safety team). Anywhere a path takes {id} you can pass the id or the short code.
Create a code
curl -X POST https://qrtoast.com/api/v1/codes \
-H "Authorization: Bearer $QRTOAST_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://mybakery.com/menu",
"name": "Spring menu",
"folder": "Menus"
}'Returns 201 with { data: code }. Optional style takes the same design settings as the dashboard (for example {"fg":"#2A1A10","dotStyle":"rounded"}).
Safe retries with Idempotency-Key
If a request might have reached us but you didn’t get an answer, you can send it again without making a second code. Add an optional Idempotency-Key header with any value you choose, 1 to 100 printable ASCII characters with no spaces (a UUID works well):
curl -X POST https://qrtoast.com/api/v1/codes \
-H "Authorization: Bearer $QRTOAST_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-menu-2026-11-09" \
-d '{
"destination_url": "https://mybakery.com/menu",
"name": "Spring menu"
}'- The same key with the same body returns the code the first request made, with
201and the headerIdempotent-Replayed: true. That includes two requests with the same key sent at the same moment: one makes the code and the other gets it back. - The same key with a different body returns
422 idempotency_key_reused. - If the code that key made has since been deleted, the same key returns
410 code_deleted. - A key that isn’t 1 to 100 printable characters returns
422 invalid_header. - A key is saved together with its code, so a request that fails saves nothing and you can retry with the same key. Keys are kept for 1 day, then can be used again.
List codes
GET https://qrtoast.com/api/v1/codes?limit=50&paging=cursor&q=menu&folder=MenusCodes come newest first. limit is 1 to 100 (default 50); anything else returns 422. q searches names, links and short codes; folder keeps one folder.
Going through every page (recommended: cursors)
Add paging=cursor to the first request. It returns { data: [code], has_more, next_cursor, limit }. While next_cursor isn’t null, ask for the next page with it, keeping the same q and folder:
GET https://qrtoast.com/api/v1/codes?limit=50&q=menu&folder=Menus&cursor={next_cursor}Every page costs the same however far in you are, and codes made while you page never shift or repeat a page. A cursor only works for the workspace that made it; one that doesn’t fit returns 422 invalid_field. A cursor works for 7 days; an older one returns 422 cursor_expired, so start again from the first page. Cursor pages don’t count a total.
Pages by number (offset)
Pass offset (default 0, at most 10,000) instead of a cursor to get { data: [code], has_more, next_cursor, total, total_capped, limit, offset }. total counts up to 10,000 codes; when there are more it says 10000 and total_capped is true. An offset over 10,000 returns 400 offset_too_large: deep offsets get slower in a big workspace, so switch to cursors for long lists. The next_cursor in an offset page carries on from its last code. Sending both cursor and offset returns 422.
Searches (q) read every code’s name and link, so a workspace can search 60 times a minute, on top of the request limit. Going over returns 429 rate_limited with a retry-after header.
Get, update or delete a code
Get one code:
GET https://qrtoast.com/api/v1/codes/{id}Change it:
curl -X PATCH https://qrtoast.com/api/v1/codes/Ab3dE7k \
-H "Authorization: Bearer $QRTOAST_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://mybakery.com/summer"
}'Delete it:
DELETE https://qrtoast.com/api/v1/codes/{id}PATCH accepts any of destination_url, name, folder (or null), status (active or paused) and style. Every destination change is checked for safety and kept in the code’s history. DELETE returns 204.
Scan statistics
GET https://qrtoast.com/api/v1/codes/{id}/stats?days=30Returns total_scans, daily_unique_scanners (each visitor counted once per day, summed over the range, since visitor ids rotate daily for privacy), uncounted_repeat_scans (fast repeat scans left out of the totals, explained below), a daily series and breakdowns by country, city, device, operating system and browser. days is capped at your plan’s scan history.
Repeat scans from the same device and network (the same browser on the same connection) count at most 3 times a minute per code, and one network counts at most 300 scans an hour per code. Anything over that is added to uncounted_repeat_scans instead. People who share one network and the same phone model can be counted as one person.
Working out statistics reads every scan in the range, so each workspace can ask for statistics 30 times a minute (dashboard pages included), on top of the request limit, and works out at most two at a time. Going over the minute’s allowance returns 429 rate_limited; two already being worked out returns 429 stats_busy. Both carry a retry-after header with the seconds to wait.
Code image
GET https://qrtoast.com/api/v1/codes/{id}/qrReturns the code as an SVG in its saved design, ready to drop into print templates.
Service health
GET https://qrtoast.com/api/healthFor uptime monitors. No key needed. Returns 200 when everything is working and 503 when something needs attention:
{
"status": "ok"
}status is ok when everything is working, otherwise degraded (and the response is a 503). Watch the status code or this one field. The response also carries a checks object with our own per-part details; its names can change, so don’t build alerts on them.