{"openapi":"3.1.0","info":{"title":"OpenQR API","version":"1.0.0","description":"Free REST API for generating QR codes and managing dynamic (editable) codes with scan analytics. All endpoints require a Bearer API key — create one free at https://openqr.uk/api, then send it as `Authorization: Bearer oqr_…`.\n\n## Error catalogue\nEvery error response is JSON: `{ \"error\": \"<human-readable message>\", \"code\": \"<stable machine-readable reason>\" }` — branch on `code` (one of unauthorized, invalid_request, plan_limit_exceeded, rate_limited, slug_taken, unsafe_destination, not_found), never on the message text. Status codes used across the API:\n\n- **400 Bad Request** — invalid JSON body, a missing/invalid required field (e.g. `data` is required), or a disallowed dynamic-code destination. Destinations must be public `http(s)` URLs: private/internal/loopback hosts and `oqr.to` self-loops are rejected (e.g. `Destinations can't point to private or internal addresses.`).\n- **401 Unauthorized** — the `Authorization: Bearer oqr_…` header is missing or the key is invalid/revoked.\n- **404 Not Found** — the referenced code, folder or theme doesn't exist or isn't owned by your key.\n- **409 Conflict** — a requested custom `slug` is already taken by another code.\n- **429 Too Many Requests** — dynamic-code creation rate limit hit (300 codes per hour, per user). Each created code consumes one unit of the rolling window: a bulk request consumes one unit per code, so it is rejected up-front (nothing is created) when its row count exceeds the remaining budget. Code-creation responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (unix seconds); a 429 also includes `Retry-After`.\n- **500 Internal Server Error** — an unexpected server-side failure.","contact":{"name":"OpenQR","url":"https://openqr.uk/api"},"license":{"name":"AGPL-3.0","url":"https://www.gnu.org/licenses/agpl-3.0.html"}},"servers":[{"url":"https://openqr.uk","description":"Production"}],"security":[{"bearerAuth":[]}],"paths":{"/v1":{"get":{"operationId":"discovery","summary":"API discovery index","description":"Public, unauthenticated. Returns the API version, links to the docs/OpenAPI spec/MCP server, the auth scheme, the rate-limit summary, and a map of every endpoint.","security":[],"responses":{"200":{"description":"Discovery document.","content":{"application/json":{"example":{"name":"OpenQR API","version":"1.0.0","documentation":"https://openqr.uk/api","openapi":"https://openqr.uk/v1/openapi.json","mcp":"https://openqr.uk/mcp","authentication":{"type":"bearer","header":"Authorization: Bearer oqr_…","get_a_key":"https://openqr.uk/api"},"rate_limit":{"scope":"dynamic-code creation","limit_per_hour":300,"headers":["X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset"]},"endpoints":{"GET /v1/qr":"Generate a QR image (SVG or PNG) from query params."}}}}}}}},"/v1/me":{"get":{"operationId":"getMe","summary":"Get the account that owns your API key","description":"Returns your own account identity — email, name and signup date — plus the plan, limits, usage and features the account resolves to, so plan-aware clients never hardcode limits. Useful as a connection/auth check.","responses":{"200":{"description":"Your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"u_9f8b2c1a4e7d4a90","email":"you@example.com","name":"Ada Lovelace","created_at":"2026-01-14T09:21:08.000Z","plan":"free","enforced":true,"limits":{"dynamic_codes":1,"scan_analytics_days":7,"detailed_analytics":false},"usage":{"active_dynamic":1},"features":{"protection":false,"aliases":false,"api":true}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/account/subdomain":{"get":{"operationId":"getSubdomain","summary":"Get your branded subdomain","description":"Returns the branded subdomain claimed on your account, if any. `suspended` is true when the name is still reserved for you but is not currently serving because your plan isn't Pro — in that state a request to the branded host redirects to the plain short link rather than failing, so printed codes keep working.","responses":{"200":{"description":"The branded subdomain on your account.","content":{"application/json":{"schema":{"type":"object","properties":{"subdomain":{"type":["string","null"],"description":"The claimed label, or null."},"host":{"type":["string","null"],"description":"The full branded host, or null."},"suspended":{"type":"boolean","description":"Claimed but not currently serving (not on Pro)."}}},"example":{"subdomain":"acme","host":"acme.oqr.to","suspended":false}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"operationId":"setSubdomain","summary":"Claim or change your branded subdomain (Pro)","description":"Claims `{subdomain}.oqr.to` so your codes resolve there as well as on the plain short link. Pro only. Your codes keep working on the plain short link either way, so changing this never breaks anything already printed. Every one of your codes is re-synced to the edge, and `codes_updated` reports how many.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subdomain"],"properties":{"subdomain":{"type":"string","description":"3–32 letters, numbers or hyphens; can't start or end with a hyphen, or contain a double hyphen. Lowercased — hostnames are case-insensitive."}}},"example":{"subdomain":"acme"}}}},"responses":{"200":{"description":"The subdomain is claimed and live.","content":{"application/json":{"schema":{"type":"object","properties":{"subdomain":{"type":"string"},"host":{"type":"string"},"suspended":{"type":"boolean"},"codes_updated":{"type":"integer"}}},"example":{"subdomain":"acme","host":"acme.oqr.to","suspended":false,"codes_updated":12}}}},"400":{"description":"Invalid or reserved subdomain, or not on the Pro plan."},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"That subdomain is already taken by another account."},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"operationId":"clearSubdomain","summary":"Release your branded subdomain","description":"Releases the name. Your codes stay live on the plain short link; only the branded host stops resolving. The name becomes claimable by anyone else, so releasing is not reversible — this is different from the automatic suspension that happens when a Pro plan lapses, which keeps the name reserved for you.","responses":{"200":{"description":"The subdomain is released.","content":{"application/json":{"schema":{"type":"object","properties":{"subdomain":{"type":["string","null"]},"host":{"type":["string","null"]},"suspended":{"type":"boolean"},"codes_updated":{"type":"integer"}}},"example":{"subdomain":null,"host":null,"suspended":false,"codes_updated":12}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/qr":{"get":{"operationId":"generateQrGet","summary":"Generate a QR code (query params)","description":"Returns a QR code as SVG or PNG. Encode any text or URL via the `data` parameter.","parameters":[{"name":"data","in":"query","required":true,"schema":{"type":"string","maxLength":2000},"example":"https://openqr.uk","description":"Text or URL to encode."},{"name":"format","in":"query","schema":{"type":"string","enum":["svg","png"],"default":"svg"},"example":"png"},{"name":"size","in":"query","schema":{"type":"integer","minimum":96,"maximum":2048,"default":512},"example":512,"description":"Pixel size (PNG) or viewBox (SVG)."},{"name":"margin","in":"query","schema":{"type":"integer","minimum":0,"maximum":16,"default":4},"example":4},{"name":"dark","in":"query","schema":{"type":"string"},"example":"232E3A","description":"Foreground colour as a hex code, e.g. 232E3A."},{"name":"light","in":"query","schema":{"type":"string"},"example":"FFFFFF","description":"Background colour as a hex code, e.g. FFFFFF."},{"name":"theme","in":"query","schema":{"type":"string"},"example":"Brand","description":"A saved theme id or name. Applies the theme's colours/margin. Explicit dark/light/margin override it."}],"responses":{"200":{"description":"The rendered QR code.","content":{"image/svg+xml":{"schema":{"type":"string"},"example":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">…</svg>"},"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"generateQrPost","summary":"Generate a QR code (JSON body)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"string","maxLength":2000,"description":"Text or URL to encode."},"format":{"type":"string","enum":["svg","png"],"default":"svg"},"size":{"type":"integer","minimum":96,"maximum":2048,"default":512},"margin":{"type":"integer","minimum":0,"maximum":16,"default":4},"dark":{"type":"string","description":"Foreground hex, e.g. 232E3A."},"light":{"type":"string","description":"Background hex, e.g. FFFFFF."},"theme":{"type":"string","description":"A saved theme id or name (applies its colours/margin)."}}},"example":{"data":"https://openqr.uk","format":"png","size":512,"margin":4,"dark":"232E3A","light":"FFFFFF"}}}},"responses":{"200":{"description":"The rendered QR code.","content":{"image/svg+xml":{"schema":{"type":"string"},"example":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">…</svg>"},"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/codes":{"get":{"operationId":"listCodes","summary":"List your saved codes — dynamic and static","description":"Returns every code in your library, newest first. `short_url` is null for static codes: their payload is encoded directly, so there is nothing to redirect. `/v1/dynamic` remains available as the dynamic-only view.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":200},"example":200},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from `next_cursor`; omit for the first page."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match on label, slug and destination."}],"responses":{"200":{"description":"Your codes.","content":{"application/json":{"schema":{"type":"object","properties":{"codes":{"type":"array","items":{"$ref":"#/components/schemas/SavedCode"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null when this is the last page."}}},"example":{"codes":[{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","type":"url","dynamic":true,"destination":"https://example.com/menu","label":"Spring menu","status":"active","short_url":"https://oqr.to/spring-menu","created_at":"2026-06-20T14:32:08.000Z"},{"id":"5c4d3e2f-1a2b-4c3d-8e5f-6a7b8c9d0e1f","type":"wifi","dynamic":false,"destination":"WIFI:T:WPA;S:Cafe Guest;P:latte4life;;","label":"Guest wifi","status":"active","short_url":null,"created_at":"2026-09-01T10:15:00.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createStaticCode","summary":"Save a static QR code from structured fields","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Make the create retry-safe: a timeout can be retried with the same key without minting a second code. The first 2xx response is replayed for 24h."}],"description":"Builds the payload server-side from `fields` and saves it as a static code: the content is encoded directly into the QR image, with no short link and no scan analytics. Static codes cannot be edited afterwards — save a new one to change the content. Field names per type: `url`: url · `text`: text · `email`: email, subject?, body? · `phone`: phone · `sms`: phone, message? · `whatsapp`: phone (country code, digits only), message? · `wifi`: ssid, password?, encryption? (WPA|WEP|nopass), hidden? · `geo`: lat, lng · `vcard`: firstName?, lastName?, phone?, email?, org?, title?, url?, address? (at least one of name/phone/email). Render the returned `payload` as an image via `/v1/qr`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","fields"],"properties":{"type":{"type":"string","enum":["url","text","email","phone","sms","whatsapp","wifi","geo","vcard"],"description":"The payload type."},"fields":{"type":"object","additionalProperties":{"type":["string","boolean","number"]},"description":"Field values for the type — see the per-type field list above."},"label":{"type":"string","description":"Optional label for your dashboard."},"theme":{"type":"string","description":"A saved theme id or name to style the code with."}}},"example":{"type":"wifi","fields":{"ssid":"Cafe Guest","password":"latte4life","encryption":"WPA"},"label":"Guest wifi"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaticCode"},"example":{"id":"5c4d3e2f-1a2b-4c3d-8e5f-6a7b8c9d0e1f","type":"wifi","payload":"WIFI:T:WPA;S:Cafe Guest;P:latte4life;;","label":"Guest wifi","next":"GET /v1/qr?data=WIFI%3AT%3AWPA%3BS%3ACafe%20Guest%3BP%3Alatte4life%3B%3B renders it."}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/dynamic":{"get":{"operationId":"listDynamicCodes","summary":"List your dynamic (editable) QR codes","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":200},"example":200},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from `next_cursor`; omit for the first page."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match on label, slug and destination."}],"responses":{"200":{"description":"Your dynamic codes.","content":{"application/json":{"schema":{"type":"object","properties":{"codes":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/DynamicCode"},{"type":"object","properties":{"label":{"type":["string","null"]},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}]}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null when this is the last page."}}},"example":{"codes":[{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"spring-menu","short_url":"https://oqr.to/spring-menu","destination":"https://example.com/menu","label":"Spring menu","status":"active","created_at":"2026-06-20T14:32:08.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createDynamicCode","summary":"Create a dynamic (editable) QR code","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Make the create retry-safe: a timeout can be retried with the same key without minting a second code. The first 2xx response is replayed for 24h."}],"description":"Creates a short link on oqr.to that you can re-point later. Encode the returned `short_url` in a QR code to print.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destination"],"properties":{"destination":{"type":"string","format":"uri","description":"The http(s) URL the code redirects to."},"label":{"type":"string","description":"Optional label for your dashboard."},"theme":{"type":"string","description":"A saved theme id or name to style the code with."}}},"example":{"destination":"https://example.com/menu","label":"Spring menu","theme":"Brand"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DynamicCode"},"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"spring-menu","short_url":"https://oqr.to/spring-menu","destination":"https://example.com/menu"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/dynamic/bulk":{"post":{"operationId":"bulkCreateDynamicCodes","summary":"Create up to 200 dynamic codes at once","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Make the batch retry-safe: a timeout can be retried with the same key without re-running it. The first 2xx response is replayed for 24h."}],"description":"Each code consumes one unit of the rolling code-creation rate limit (300/hour, per user). If the number of codes in the request exceeds the remaining budget the whole request is rejected with 429 (nothing is created); otherwise the 201 response carries `X-RateLimit-*` headers reflecting the consumed budget.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["codes"],"properties":{"codes":{"type":"array","maxItems":200,"items":{"type":"object","required":["destination"],"properties":{"destination":{"type":"string","format":"uri"},"label":{"type":"string"}}}},"theme":{"type":"string","description":"A saved theme id or name applied to every code created."}}},"example":{"codes":[{"destination":"https://example.com/menu","label":"Spring menu"},{"destination":"https://example.com/wifi","label":"Guest wifi"}],"theme":"Brand"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"array","items":{"$ref":"#/components/schemas/DynamicCode"}}}},"example":{"created":[{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"k7m2qd","short_url":"https://oqr.to/k7m2qd","destination":"https://example.com/menu","label":"Spring menu"},{"id":"1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9","slug":"p9x4rt","short_url":"https://oqr.to/p9x4rt","destination":"https://example.com/wifi","label":"Guest wifi"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/dynamic/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getDynamicCode","summary":"Get one code's full details","description":"Fetch a single code you own by id: destination, short link, label, type, tags, folder, status and created date.","responses":{"200":{"description":"The code.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"short_url":{"type":"string","format":"uri"},"destination":{"type":"string"},"label":{"type":["string","null"]},"type":{"type":"string"},"dynamic":{"type":"boolean"},"status":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"folder_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"summer-menu","short_url":"https://oqr.to/summer-menu","destination":"https://example.com/summer-menu","label":"Summer menu","type":"url","dynamic":true,"status":"active","tags":["menu","spring"],"folder_id":null,"created_at":"2026-08-05T01:00:00.000Z"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"operationId":"updateDynamicCode","summary":"Edit a dynamic code (any field the dashboard can edit)","description":"Send only the fields you want to change: destination, label, custom slug, tags, folder or style.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination":{"type":"string","format":"uri"},"label":{"type":["string","null"]},"slug":{"type":"string","description":"Custom short-link back-half: 3–48 letters, numbers or hyphens."},"tags":{"type":"array","items":{"type":"string"},"description":"Replaces the code's tags (max 10)."},"folder_id":{"type":["string","null"],"description":"Folder id to file under, or null to un-file."},"style":{"type":"object","description":"QR style JSON (colours, dot/corner shapes, etc.) for re-download."},"theme":{"type":"string","description":"A saved theme id or name to restyle the code with (sets style)."},"password":{"type":["string","null"],"description":"Pro: require this password (min 4 characters) before the code redirects. Pass null to remove all protection."},"pin":{"type":["string","null"],"description":"Pro: require a 4-digit PIN before the code redirects. Exactly 4 digits. Pass null to remove all protection."},"lock_type":{"type":"string","enum":["pin","password"],"description":"Which input scanners see."},"lock_hint":{"type":["string","null"],"description":"Optional hint shown on the unlock page, e.g. \"Ask at the bar\". Max 60 characters."},"ios_destination":{"type":["string","null"],"format":"uri","description":"App links: iOS scanners follow this URL instead of destination (e.g. the App Store page). Null clears the override. Applies to the plain redirect path only."},"android_destination":{"type":["string","null"],"format":"uri","description":"App links: Android scanners follow this URL instead of destination (e.g. the Google Play page). Null clears the override."},"pause_until":{"type":["string","null"],"format":"date-time","description":"Schedule a pause: while this timestamp is in the future the code serves its inactive page, then self-resumes at the deadline (no cron, no status change). Pass null to clear the schedule. Future timestamps only, active codes only."}}},"example":{"destination":"https://example.com/summer-menu","label":"Summer menu","slug":"summer-menu"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/DynamicCode"},{"type":"object","properties":{"label":{"type":["string","null"]}}}]},"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"summer-menu","short_url":"https://oqr.to/summer-menu","destination":"https://example.com/summer-menu","label":"Summer menu"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"operationId":"deleteDynamicCode","summary":"Delete a dynamic code","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/dynamic/{id}/aliases":{"get":{"operationId":"listDynamicCodeAliases","summary":"List the link aliases of a dynamic code","description":"A link alias is an extra oqr.to slug that resolves where the code points. Share a different slug per channel while the QR keeps its own, so QR scans and link clicks are counted separately. Aliases have their own per-plan allowance (free 0, Pro 10) and never consume the editable-destination allowance.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Alias list.","content":{"application/json":{"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"spring-menu","short_url":"https://oqr.to/spring-menu","aliases":[{"id":"1a2b3c4d-…","slug":"spring-flyer","short_url":"https://oqr.to/spring-flyer","created_at":"2026-09-09T12:00:00.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"operationId":"createDynamicCodeAlias","summary":"Create a link alias for a dynamic code","description":"The alias resolves where the code points and follows its destination edits, pauses and lock state. Custom slugs share the same namespace as code short links and must be free there too.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Optional custom back-half (oqr.to/<slug>), 3–48 letters, numbers or hyphens. Omit for a random one."}}}}}},"responses":{"201":{"description":"Alias created.","content":{"application/json":{"example":{"id":"1a2b3c4d-…","slug":"spring-flyer","short_url":"https://oqr.to/spring-flyer"}}}},"400":{"description":"The slug is taken/reserved, the code is static, or the alias allowance is exhausted (free accounts have none; Pro includes 10)."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/dynamic/{id}/aliases/{aliasId}":{"delete":{"operationId":"deleteDynamicCodeAlias","summary":"Delete a link alias","description":"The alias's short link stops resolving immediately; the code itself is unaffected.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"aliasId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/dynamic/{id}/scans":{"get":{"operationId":"getDynamicCodeScans","summary":"Get scan analytics for a dynamic code","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c"},{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":365,"default":30},"example":30}],"responses":{"200":{"description":"Scan summary.","content":{"application/json":{"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"spring-menu","short_url":"https://oqr.to/spring-menu","destination":"https://example.com/menu","scans":{"total":1280,"last7":214,"topCountry":"GB","topDevice":"mobile"},"analytics":{"days_window":30,"total":1280,"window_total":642,"daily":[{"day":"2026-06-25","n":41},{"day":"2026-06-26","n":58}],"by_country":[{"value":"GB","n":402},{"value":"US","n":188}],"by_device":[{"value":"mobile","n":511},{"value":"desktop","n":131}],"by_referrer":[{"value":"instagram.com","n":273},{"value":"direct","n":369}]}},"schema":{"allOf":[{"$ref":"#/components/schemas/DynamicCode"},{"type":"object","properties":{"scans":{"type":"object","properties":{"total":{"type":"integer"},"last7":{"type":"integer"},"topCountry":{"type":["string","null"]},"topDevice":{"type":["string","null"]}}},"analytics":{"type":"object","description":"Detailed breakdowns over the window (default 30 days, ?days=).","properties":{"days_window":{"type":"integer"},"total":{"type":"integer"},"window_total":{"type":"integer"},"daily":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string"},"n":{"type":"integer"}}}},"by_country":{"$ref":"#/components/schemas/Breakdown"},"by_device":{"$ref":"#/components/schemas/Breakdown"},"by_referrer":{"$ref":"#/components/schemas/Breakdown"}}}}}]}}}},"400":{"description":"The id belongs to a static code: static codes carry their content directly and never record scans, so scan analytics exist for editable (dynamic) codes only."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/themes":{"get":{"operationId":"listThemes","summary":"List your saved style themes","description":"Apply a theme by passing its id or name as `theme` to the QR-generation and dynamic-code endpoints.","responses":{"200":{"description":"Your themes.","content":{"application/json":{"schema":{"type":"object","properties":{"themes":{"type":"array","items":{"$ref":"#/components/schemas/Theme"}}}},"example":{"themes":[{"id":"b1c2d3e4-f5a6-4071-8293-a4b5c6d7e8f9","name":"Brand","style":{"fgColor":"#232E3A","bgColor":"#FFFFFF","dotType":"rounded","margin":4}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createTheme","summary":"Save a reusable style theme","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","style"],"properties":{"name":{"type":"string"},"style":{"type":"object","description":"QR style JSON (fgColor, bgColor, dotType, cornerSquareType, logo, gradient, frame…)."}}},"example":{"name":"Brand","style":{"fgColor":"#232E3A","bgColor":"#FFFFFF","dotType":"rounded","cornerSquareType":"extra-rounded","margin":4}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"theme":{"$ref":"#/components/schemas/Theme"}}},"example":{"theme":{"id":"b1c2d3e4-f5a6-4071-8293-a4b5c6d7e8f9","name":"Brand","style":{"fgColor":"#232E3A","bgColor":"#FFFFFF","dotType":"rounded","cornerSquareType":"extra-rounded","margin":4}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/themes/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"b1c2d3e4-f5a6-4071-8293-a4b5c6d7e8f9"}],"delete":{"operationId":"deleteTheme","summary":"Delete a saved theme (styled codes keep their look)","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/folders":{"get":{"operationId":"listFolders","summary":"List your folders","responses":{"200":{"description":"Your folders.","content":{"application/json":{"schema":{"type":"object","properties":{"folders":{"type":"array","items":{"$ref":"#/components/schemas/Folder"}}}},"example":{"folders":[{"id":"c3d4e5f6-a7b8-4091-8293-a4b5c6d7e8f9","name":"Campaigns"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createFolder","summary":"Create a folder","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"example":{"name":"Campaigns"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"},"example":{"id":"c3d4e5f6-a7b8-4091-8293-a4b5c6d7e8f9","name":"Campaigns"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/v1/folders/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"c3d4e5f6-a7b8-4091-8293-a4b5c6d7e8f9"}],"patch":{"operationId":"renameFolder","summary":"Rename a folder","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"example":{"name":"Q3 Campaigns"}}}},"responses":{"200":{"description":"Renamed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"},"example":{"id":"c3d4e5f6-a7b8-4091-8293-a4b5c6d7e8f9","name":"Q3 Campaigns"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"operationId":"deleteFolder","summary":"Delete a folder (its codes are un-filed, not deleted)","responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"oqr_…","description":"Your OpenQR API key, created at /api. Send it as `Authorization: Bearer oqr_…` (example key: `oqr_8sKf3mTnQ2pXvB6hLrYwD4gJzCe7aUqM9NdRtPx`)."}},"schemas":{"SavedCode":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","description":"Payload type: url, text, email, phone, sms, whatsapp, wifi, geo or vcard."},"dynamic":{"type":"boolean","description":"True for editable short-link codes; false for static codes with a baked payload."},"destination":{"type":"string","description":"The redirect target (dynamic) or the encoded payload (static)."},"label":{"type":["string","null"]},"status":{"type":"string"},"short_url":{"type":["string","null"],"description":"The oqr.to short link, or null for static codes."},"created_at":{"type":"string","format":"date-time"}}},"StaticCode":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"payload":{"type":"string","description":"The encoded payload string (what the QR image contains)."},"label":{"type":["string","null"]},"next":{"type":"string","description":"Render the payload via GET /v1/qr?data=<payload>."}}},"DynamicCode":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"short_url":{"type":"string","format":"uri"},"destination":{"type":"string","format":"uri"}},"example":{"id":"9f8b2c1a-4e7d-4a90-b2f1-0c3d5e6a7b8c","slug":"spring-menu","short_url":"https://oqr.to/spring-menu","destination":"https://example.com/menu"}},"Folder":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"example":{"id":"c3d4e5f6-a7b8-4091-8293-a4b5c6d7e8f9","name":"Campaigns"}},"Theme":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"style":{"type":"object","description":"The saved QR style JSON."}},"example":{"id":"b1c2d3e4-f5a6-4071-8293-a4b5c6d7e8f9","name":"Brand","style":{"fgColor":"#232E3A","bgColor":"#FFFFFF","dotType":"rounded","margin":4}}},"Breakdown":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"n":{"type":"integer"}}},"example":[{"value":"GB","n":402},{"value":"US","n":188}]},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message."},"code":{"type":"string","description":"Stable machine-readable reason — branch on this, never on the message or the HTTP status alone.","enum":["unauthorized","invalid_request","plan_limit_exceeded","rate_limited","slug_taken","unsafe_destination","not_found"]}},"example":{"error":"Invalid request.","code":"invalid_request"}},"Account":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"plan":{"type":"string","enum":["free","pro","business"],"description":"The plan the account resolves to."},"enforced":{"type":"boolean","description":"True when plan caps are actively enforced; false = dormant billing (limits are advisory)."},"limits":{"type":"object","properties":{"dynamic_codes":{"type":["integer","null"],"description":"Active-dynamic-code cap; null = unlimited."},"scan_analytics_days":{"type":["integer","null"],"description":"Analytics history window; null = unlimited."},"detailed_analytics":{"type":"boolean","description":"Whether device/referrer/region dimensions are included in scan responses."}}},"usage":{"type":"object","properties":{"active_dynamic":{"type":"integer","description":"Currently active dynamic codes, against limits.dynamic_codes."}}},"features":{"type":"object","properties":{"protection":{"type":"boolean","description":"PIN/password-protected codes available."},"aliases":{"type":"boolean","description":"Link aliases available."},"api":{"type":"boolean","description":"REST/MCP API access (true on every plan)."}}}}}},"responses":{"BadRequest":{"description":"Invalid request — malformed JSON, a missing/invalid required field, or a disallowed dynamic-code destination (destinations must be public http(s) URLs; private/internal hosts and oqr.to self-loops are rejected).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missingField":{"summary":"Missing required field","value":{"error":"`data` is required"}},"invalidJson":{"summary":"Malformed body","value":{"error":"Invalid JSON body"}},"unsafeScheme":{"summary":"Non-http(s) destination","value":{"error":"Destination must be an http(s) URL."}},"internalHost":{"summary":"Private/internal destination","value":{"error":"Destinations can't point to private or internal addresses."}},"selfLoop":{"summary":"Self-loop destination","value":{"error":"A code can't redirect to another short link."}}}}}},"Unauthorized":{"description":"The `Authorization: Bearer oqr_…` header is missing or the API key is invalid or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unauthorized — provide a Bearer API key. Send `Authorization: Bearer oqr_…` (create one free at https://openqr.uk/api)."}}}},"NotFound":{"description":"The referenced code, folder or theme doesn't exist or isn't owned by your API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}},"Conflict":{"description":"The requested custom `slug` is already taken by another code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"That short link is already taken."}}}},"RateLimited":{"description":"Dynamic-code creation rate limit hit — 300 codes per hour, per user. Bulk requests consume one unit per code and are rejected up-front (nothing created) when they exceed the remaining budget.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Too many codes created recently. Try again later."}}}},"ServerError":{"description":"An unexpected server-side error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error"}}}}}}}