{"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>\" }`. 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 (20 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":20,"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. Useful as a connection/auth check.","responses":{"200":{"description":"Your account.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"example":{"id":"u_9f8b2c1a4e7d4a90","email":"you@example.com","name":"Ada Lovelace","created_at":"2026-01-14T09:21:08.000Z"}}}},"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":64,"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 (SVG). 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":64,"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/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}],"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"}}}]}}}},"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","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","description":"Each code consumes one unit of the rolling code-creation rate limit (20/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"}}],"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)."}}},"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}/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"}}}}}]}}}},"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":{"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."}},"example":{"error":"Invalid request."}}},"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 — 20 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"}}}}}}}