Skip to content

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."
  }
}

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"
  }'

List codes

GET https://qrtoast.com/api/v1/codes?limit=50&paging=cursor&q=menu&folder=Menus

Codes 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=30

Returns 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}/qr

Returns the code as an SVG in its saved design, ready to drop into print templates.

Service health

GET https://qrtoast.com/api/health

For 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.