{"openapi":"3.1.0","info":{"title":"QR Hub API","version":"1.0.0","description":"Create and manage QR codes, and read their scan data, from your own systems.\n\n## Authentication\n\nEvery request needs a workspace API key as a bearer token:\n\n```\nAuthorization: Bearer qr_live_xxxxxxxxxxxxxxxx\n```\n\nCreate one in **Settings \u2192 API keys**. The plaintext is shown exactly once at\ncreation and stored only as a SHA-256 hash, so it cannot be recovered later \u2014\nif you lose it, revoke the key and issue another.\n\nA key is bound to one workspace. Every response is scoped to that workspace, and\na code belonging to another one returns `404`, never `403` \u2014 the API does not\nconfirm the existence of resources your key cannot see.\n\n## Plans\n\nAPI access is a **Pro** capability. A key on a Free workspace authenticates but\nreceives `402 Payment Required` on every call, as does a key issued while on Pro\nafter the workspace downgrades. Some write operations are also plan-limited: see\n`402` on the individual endpoints.\n\n## Rate limiting\n\n120 requests per minute per key. Responses carry `X-RateLimit-Limit` and\n`X-RateLimit-Remaining`; exceeding it returns `429` with a `Retry-After` header.\n\n## Errors\n\nFailures return a JSON object with a `message`. Validation failures (`422`) also\ncarry an `errors` object keyed by field name."},"servers":[{"url":"https:\/\/qrhub.cc\/api\/v1"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Account","description":"Verify a key and read plan usage."},{"name":"Codes","description":"Create, read, update and delete QR codes and short links. A short link is a code with `kind: \"link\"` \u2014 a tracked, editable URL with no image."},{"name":"Scans","description":"Scan history and aggregates for a code."}],"paths":{"\/me":{"get":{"tags":["Account"],"summary":"Verify a key and read workspace usage","description":"The quickest way to confirm a token works and see what plan and code budget it has.","responses":{"200":{"description":"Workspace and usage.","content":{"application\/json":{"example":{"workspace":{"id":1,"name":"Acme","plan":"pro"},"usage":{"codes":12,"code_limit":null}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application\/json":{"example":{"message":"Missing or invalid bearer token."}}}},"402":{"description":"API access is available on the Pro plan.","content":{"application\/json":{"example":{"message":"API access is available on the Pro plan."}}}}}}},"\/codes":{"get":{"tags":["Codes"],"summary":"List codes","parameters":[{"name":"kind","description":"QR codes or short links.","in":"query","required":false,"schema":{"type":"string","enum":["qr","link"]}},{"name":"status","description":"Filter by status.","in":"query","required":false,"schema":{"type":"string","enum":["active","paused","archived"]}},{"name":"folder","description":"Filter by folder name.","in":"query","required":false,"schema":{"type":"string"}},{"name":"per_page","description":"Results per page (1\u2013100). Defaults to 25.","in":"query","required":false,"schema":{"type":"integer"}},{"name":"page","description":"Page number.","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"A page of codes.","content":{"application\/json":{"example":{"data":[{"id":42,"kind":"qr","name":"Menu poster","type":"url","content":"https:\/\/example.com\/menu","type_data":null,"is_dynamic":true,"short_code":"aB3xK9","redirect_url":"https:\/\/qrhub.cc\/r\/aB3xK9","encoded_value":"https:\/\/qrhub.cc\/r\/aB3xK9","status":"active","folder":"Restaurant","style_config":{"fg_color":"#111827"},"scan_count":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","created_at":"2026-08-01T10:00:00+00:00","updated_at":"2026-08-17T09:15:00+00:00"}],"links":{"first":"\u2026","last":"\u2026","prev":null,"next":"\u2026"},"meta":{"current_page":1,"per_page":25,"total":12,"last_page":1}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application\/json":{"example":{"message":"Missing or invalid bearer token."}}}},"402":{"description":"API access is available on the Pro plan.","content":{"application\/json":{"example":{"message":"API access is available on the Pro plan."}}}}}},"post":{"tags":["Codes"],"summary":"Create a code or short link","description":"A dynamic code's `content` must be an http(s) URL \u2014 it is served as a redirect, so any other scheme would be dereferenced from our origin.\n\nFor `url`, `pdf` and `menu`, send the link as `content`. Every other type takes its fields in `type_data` and `content` is built from them: `text` {text}; `email` {to, subject?, body?}; `phone` {number}; `sms` {number, message?}; `wifi` {ssid, encryption: WPA|WEP|nopass, password?, hidden?}; `vcard` {first_name, last_name?, organization?, title?, phone?, email?, website?, address?}; `location` {query}; `app` {ios_url?, android_url?, fallback_url}.\n\n`text`, `email`, `phone`, `sms`, `wifi` and `vcard` are read by the phone itself, so they cannot be dynamic. `app` is always dynamic: each scan is sent to the App Store, Google Play or `fallback_url` by device.\n\nSend `kind: \"link\"` to create a short link: it is always a dynamic `url`, and `type`, `is_dynamic` and `style_config` are ignored.","requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["qr","link"],"default":"qr"},"name":{"type":"string","maxLength":255,"example":"Menu poster"},"type":{"type":"string","enum":["url","text","email","phone","sms","wifi","vcard","location","app","pdf","menu"],"example":"url"},"content":{"type":"string","example":"https:\/\/example.com\/menu","description":"The link, for url \/ pdf \/ menu. Built from `type_data` for every other type."},"type_data":{"type":"object","additionalProperties":true,"example":{"ssid":"Cafe Guest","encryption":"WPA","password":"latte"}},"is_dynamic":{"type":"boolean","default":false,"description":"Dynamic codes redirect through a short link you can repoint later without reprinting."},"style_config":{"type":"object","additionalProperties":true,"description":"Colours and shapes. A `logo` key is ignored unless the plan includes logos."},"folder":{"type":"string","maxLength":100,"description":"Folder name. Created if it does not exist."},"short_code":{"type":"string","description":"Custom short-link slug. Omit to have one generated."},"status":{"type":"string","enum":["active","paused","archived"]}},"required":["name","type"]}}}},"responses":{"201":{"description":"The created code.","content":{"application\/json":{"example":{"id":42,"kind":"qr","name":"Menu poster","type":"url","content":"https:\/\/example.com\/menu","type_data":null,"is_dynamic":true,"short_code":"aB3xK9","redirect_url":"https:\/\/qrhub.cc\/r\/aB3xK9","encoded_value":"https:\/\/qrhub.cc\/r\/aB3xK9","status":"active","folder":"Restaurant","style_config":{"fg_color":"#111827"},"scan_count":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","created_at":"2026-08-01T10:00:00+00:00","updated_at":"2026-08-17T09:15:00+00:00"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application\/json":{"example":{"message":"Missing or invalid bearer token."}}}},"402":{"description":"You've reached the 5-item limit (QR codes and short links) on the Free plan. Upgrade your plan to add more.","content":{"application\/json":{"example":{"message":"You've reached the 5-item limit (QR codes and short links) on the Free plan. Upgrade your plan to add more.","code_limit":5}}}},"422":{"description":"Validation failed.","content":{"application\/json":{"example":{"message":"The given data was invalid.","errors":{"content":["A dynamic code must point at an http or https URL."]}}}}}}}},"\/codes\/{code}":{"parameters":[{"name":"code","description":"The code id.","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Codes"],"summary":"Fetch a code","responses":{"200":{"description":"The code.","content":{"application\/json":{"example":{"id":42,"kind":"qr","name":"Menu poster","type":"url","content":"https:\/\/example.com\/menu","type_data":null,"is_dynamic":true,"short_code":"aB3xK9","redirect_url":"https:\/\/qrhub.cc\/r\/aB3xK9","encoded_value":"https:\/\/qrhub.cc\/r\/aB3xK9","status":"active","folder":"Restaurant","style_config":{"fg_color":"#111827"},"scan_count":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","created_at":"2026-08-01T10:00:00+00:00","updated_at":"2026-08-17T09:15:00+00:00"}}}},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}}}},"put":{"tags":["Codes"],"summary":"Update a code","description":"`short_code` is ignored once the code has been scanned: the slug is baked into anything already printed, so changing it would break every copy in the wild.","requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255},"type":{"type":"string","enum":["url","text","email","phone","sms","wifi","vcard","location","app","pdf","menu"],"description":"Changing type needs the new type's `type_data` (or `content` for url \/ pdf \/ menu)."},"content":{"type":"string"},"type_data":{"type":"object","additionalProperties":true},"is_dynamic":{"type":"boolean"},"style_config":{"type":"object","additionalProperties":true},"folder":{"type":["string","null"],"maxLength":100,"description":"Folder name, created if needed. Empty or null removes it from its folder."},"status":{"type":"string","enum":["active","paused","archived"]},"short_code":{"type":"string"}}}}}},"responses":{"200":{"description":"The updated code.","content":{"application\/json":{"example":{"id":42,"kind":"qr","name":"Menu poster","type":"url","content":"https:\/\/example.com\/menu","type_data":null,"is_dynamic":true,"short_code":"aB3xK9","redirect_url":"https:\/\/qrhub.cc\/r\/aB3xK9","encoded_value":"https:\/\/qrhub.cc\/r\/aB3xK9","status":"active","folder":"Restaurant","style_config":{"fg_color":"#111827"},"scan_count":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","created_at":"2026-08-01T10:00:00+00:00","updated_at":"2026-08-17T09:15:00+00:00"}}}},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}},"422":{"description":"Validation failed.","content":{"application\/json":{"example":{"message":"The given data was invalid.","errors":{"content":["A dynamic code must point at an http or https URL."]}}}}}}},"patch":{"tags":["Codes"],"summary":"Update a code (partial)","description":"Identical to `PUT`; every field is optional on both.","requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","archived"]}}}}}},"responses":{"200":{"description":"The updated code.","content":{"application\/json":{"example":{"id":42,"kind":"qr","name":"Menu poster","type":"url","content":"https:\/\/example.com\/menu","type_data":null,"is_dynamic":true,"short_code":"aB3xK9","redirect_url":"https:\/\/qrhub.cc\/r\/aB3xK9","encoded_value":"https:\/\/qrhub.cc\/r\/aB3xK9","status":"active","folder":"Restaurant","style_config":{"fg_color":"#111827"},"scan_count":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","created_at":"2026-08-01T10:00:00+00:00","updated_at":"2026-08-17T09:15:00+00:00"}}}},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}},"422":{"description":"Validation failed.","content":{"application\/json":{"example":{"message":"The given data was invalid.","errors":{"content":["A dynamic code must point at an http or https URL."]}}}}}}},"delete":{"tags":["Codes"],"summary":"Delete a code","description":"Permanent. A dynamic code stops resolving, so anything already printed that points at it stops working.","responses":{"204":{"description":"Deleted. No body."},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}}}}},"\/codes\/{code}\/scans":{"parameters":[{"name":"code","description":"The code id.","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Scans"],"summary":"List raw scans","description":"Bots are recorded but excluded by default, so the rows agree with `summary.total_scans`.","parameters":[{"name":"from","description":"Only scans at or after this timestamp.","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","description":"Only scans at or before this timestamp.","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_bots","description":"Include crawler and link-preview hits.","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"per_page","description":"Results per page (1\u2013200). Defaults to 50.","in":"query","required":false,"schema":{"type":"integer"}},{"name":"page","description":"Page number.","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"A page of scans.","content":{"application\/json":{"example":{"data":[{"id":9001,"country":"GB","city":"London","device":"mobile","browser":"Safari","os":"iOS","language":"en","referer":null,"referer_host":null,"utm_source":null,"utm_medium":null,"utm_campaign":null,"is_bot":false,"scanned_at":"2026-08-17T09:15:00+00:00"}],"meta":{"current_page":1,"per_page":50,"total":240,"last_page":5},"summary":{"total_scans":240,"last_scanned_at":"2026-08-17T09:15:00+00:00","bot_scans":18}}}}},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}},"422":{"description":"Validation failed.","content":{"application\/json":{"example":{"message":"The given data was invalid.","errors":{"content":["A dynamic code must point at an http or https URL."]}}}}}}}},"\/codes\/{code}\/analytics":{"parameters":[{"name":"code","description":"The code id.","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Scans"],"summary":"Aggregated scan stats","description":"The same figures the dashboard shows. Bots are excluded from every number here.","parameters":[{"name":"days","description":"Window in days (1\u2013365). Defaults to 30.","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Aggregates for the window.","content":{"application\/json":{"example":{"range":{"days":30,"from":"2026-07-18T00:00:00+00:00","to":"2026-08-17T00:00:00+00:00"},"totals":{"scans":240,"unique_visitors":173,"bot_scans":18},"by_country":[{"label":"GB","count":120}],"by_device":[{"label":"mobile","count":210}],"by_browser":[{"label":"Safari","count":98}],"by_os":[{"label":"iOS","count":101}],"by_referer":[{"label":"instagram.com","count":44}],"by_campaign":[{"label":"summer-menu","count":31}]}}}},"404":{"description":"Code not found.","content":{"application\/json":{"example":{"message":"Code not found."}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A workspace API key from Settings \u2192 API keys. Looks like `qr_live_\u2026`."}},"schemas":{"Code":{"type":"object","properties":{"id":{"type":"integer"},"kind":{"type":"string","enum":["qr","link"]},"name":{"type":"string"},"type":{"type":"string","enum":["url","text","email","phone","sms","wifi","vcard","location","app","pdf","menu"]},"content":{"type":"string","description":"What the code points at, or the payload built from `type_data`."},"type_data":{"type":["object","null"],"additionalProperties":true,"description":"The structured fields for non-URL types."},"is_dynamic":{"type":"boolean"},"short_code":{"type":["string","null"]},"redirect_url":{"type":["string","null"],"description":"The public short link."},"encoded_value":{"type":"string","description":"What the QR image must encode. Use this, not `content` \u2014 encoding the destination of a dynamic code produces an untrackable, unrepointable code."},"status":{"type":"string"},"folder":{"type":["string","null"]},"style_config":{"type":["object","null"],"additionalProperties":true},"scan_count":{"type":"integer","description":"Scans (QR) or clicks (link). Excludes bots."},"last_scanned_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}