# QRX API The QRX API makes artistic, scannable QR codes from other systems. Send a prompt describing a picture (or [your own picture](#own-picture)) and the link the code should open; QRX paints a code into the art, checks it scans, and gives you back a PNG. It is a plain HTTPS and JSON API, so it works from any language, and AI agents can use it through the [MCP server](https://qrx.codes/developers/docs/agents.md). ## Quick start {#quick-start} Four steps from nothing to a finished code. Each one takes a minute, and the painting usually takes under a minute too. ### 1. Create a key {#create-a-key} Make a key on the [API keys](https://qrx.codes/developers/keys) page and copy it: the secret is shown once. Keep it in an environment variable rather than in your code: ```bash export QRX_API_KEY="qrx_live_…" ``` ### 2. Create a code {#create-a-code} Give the prompt and the destination. The answer comes back at once with `202 Accepted` and the code `processing`. The `Idempotency-Key` makes a retry safe: send the same key again after a network failure and you get the first answer, not a second code. ```bash curl curl -X POST https://qrx.codes/v1/codes \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Idempotency-Key: menu-2026-10-08" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A lighthouse on a cliff at sunset", "destination": "https://example.com/menu" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'menu-2026-10-08', }, body: JSON.stringify({ prompt: 'A lighthouse on a cliff at sunset', destination: 'https://example.com/menu', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/codes", headers={"Authorization": f"Bearer {QRX_API_KEY}", "Idempotency-Key": "menu-2026-10-08"}, json={ "prompt": "A lighthouse on a cliff at sunset", "destination": "https://example.com/menu", }, ) data = response.json() ``` ```json 202 { "id": "k7m2qz", "object": "code", "status": "processing", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": null, "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z" } ``` ### 3. Wait for it {#wait} Ask for the code with `?wait=45`: the API holds the request until the code finishes, or for 45 seconds, whichever comes first. Repeat until `status` is `succeeded` (or `failed`). Most codes finish within the first wait. You can also pass `?wait=` when you create the code. ```bash curl curl "https://qrx.codes/v1/codes/k7m2qz?wait=45" \ -H "Authorization: Bearer $QRX_API_KEY" ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes/k7m2qz?wait=45', { headers: { Authorization: `Bearer ${QRX_API_KEY}`, }, }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.get( "https://qrx.codes/v1/codes/k7m2qz", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, params={ "wait": 45, }, ) data = response.json() ``` ### 4. Use the image {#use-the-image} A code that has succeeded has an `imageUrl` (a PNG to download, print or show) and a `shortUrl`, the link the code encodes. Scans go through `shortUrl` to your `destination`, so on Starter you can change where a printed code points later. ```json 200 { "id": "k7m2qz", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": "https://qrx.codes/media/premium/k7m2qz.png", "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z" } ``` > **Tip:** Every code also appears in My codes in the app, where you can download it at other sizes. ## Codes from your own picture {#own-picture} Instead of a prompt, send a picture you already have and QRX weaves the code into it: a photo, an illustration or artwork. The code is drawn into the picture's centre square, in its colours and light, and checked to scan like any other code. Each code from the same picture comes out differently. Send the picture in one of three ways: - `image`: the picture base64 encoded, or as a base64 `data:` URL, in the JSON body (up to 14 MB in all). - `imageUrl`: a public `https` link to it, which QRX downloads (default port and public addresses only, at most 3 redirects and 15 seconds). - A `multipart/form-data` body with the picture as the file part `image` and the other fields as text parts. The picture must be a PNG, JPEG or WebP of at most 10 MB, at least 256 pixels on each side and no more than 4 times as wide as it is tall (or tall as it is wide). `prompt` becomes optional: give one to say what matters in the picture, or leave it out and the picture is described for you. `destination` or `wifi`, `style`, `logo`, `callbackUrl`, `Idempotency-Key` and the plan's limits work as for any code, and the answer is the same code object. ```bash curl curl -X POST https://qrx.codes/v1/codes \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Idempotency-Key: shopfront-2026-10-13" \ -H "Content-Type: application/json" \ -d '{ "imageUrl": "https://example.com/photos/shopfront.jpg", "destination": "https://example.com/menu" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'shopfront-2026-10-13', }, body: JSON.stringify({ imageUrl: 'https://example.com/photos/shopfront.jpg', destination: 'https://example.com/menu', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/codes", headers={"Authorization": f"Bearer {QRX_API_KEY}", "Idempotency-Key": "shopfront-2026-10-13"}, json={ "imageUrl": "https://example.com/photos/shopfront.jpg", "destination": "https://example.com/menu", }, ) data = response.json() ``` ```bash multipart curl -X POST "https://qrx.codes/v1/codes" \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Idempotency-Key: shopfront-2026-10-13" \ -F "image=@shopfront.jpg" \ -F "destination=https://example.com/menu" ``` Every picture, and any prompt sent with it, is checked for safety before anything is stored or drawn. One that does not pass is answered `422` with the problem type `unsafe-content` and the detail "We can't process this image for safety reasons." Nothing is stored or counted, and the reasons are not given. Pictures are kept privately with the code and are never shown in the API, the app or the code's page. ## Base URL {#base-url} ```text https://qrx.codes/v1 ``` Requests and responses are JSON over HTTPS. Send `Content-Type: application/json` with a body (or `multipart/form-data` to [upload a picture](#own-picture)); fields the API does not know are refused rather than ignored. The full description of every endpoint is in the [API reference](https://qrx.codes/developers/docs/reference.md), and as an OpenAPI 3.1 document at `https://qrx.codes/v1/openapi.json`. | Resource | Endpoints | | --- | --- | | Codes | `POST /v1/codes`, `GET /v1/codes`, `GET /v1/codes/{id}`, `PATCH /v1/codes/{id}`, `POST /v1/codes/{id}/variations`, `POST /v1/codes/{id}/report` | | Account | `GET /v1/account`: your plan, codes left today, rate limits | | Styles | `GET /v1/styles` | | Logos | `POST /v1/logos`: a logo to place in the centre of codes, passed as `logo` when creating one | Lists are `{ "object": "list", "data": [...], "hasMore": true, "nextCursor": "…" }`: pass `nextCursor` as `cursor` for the next page. ## Variations {#variations} To make a new code from one of yours with a change to its picture, send the change in words: `POST /v1/codes/{id}/variations` with `{ "change": "more trees" }`. The variation is a new code, with its own `id` and `shortUrl`, that opens the same `destination` and keeps the original's style and logo; the original does not change. It is made, counted and checked to scan like any code, and answers like `POST /v1/codes`: send an `Idempotency-Key` and wait for it with `?wait=45`. Its `derivedFrom` names the original and the change. QRX keeps the picture behind each code it makes (privately; it is never shown or returned), and changes that picture for a variation (`"method": "edit"`), so the composition stays close to the original's. Codes made before QRX kept pictures are drawn again from their prompt with the change added (`"method": "redraw"`), so their composition can differ more. Only link codes you own that have succeeded can be varied; any other id is not found or refused. ## Finished codes {#finished-codes} Each code has a `completedAt`: when it succeeded or failed, or null while it is processing. To pick up codes as they finish, poll `GET /v1/codes?status=succeeded&completedAfter=`. Filtered to finished codes, the list is in order of `completedAt`, newest first, so a code that took longer than the others is never missed. `completedAfter` is inclusive: a code that finished at exactly that time is listed again, so keep the ids you have seen and drop repeats. Automation platforms whose steps can't wait a minute (such as Zapier) can pass `callbackUrl` when creating a code. Once the code succeeds or fails, QRX `POST`s it, as `GET /v1/codes/{id}` shows it, to that URL. The URL must be `https` on a host QRX delivers callbacks to (currently Zapier's). Any `2xx` answer counts as delivered. Anything else is retried with backoff (at most hourly) for 30 days, except a `3xx` or `4xx` other than `408`, `425` and `429`, which ends the retries; redirects are not followed. Repeating the create with the same `Idempotency-Key` sends the callback again once the code has finished. ## Authentication {#authentication} Send your key as a bearer token on every request: ```text Authorization: Bearer qrx_live_abc123def456_… ``` A client that can set a header but not an authorisation scheme can send the key as `X-API-Key: qrx_live_…` instead. Never put a key in a URL. - The secret is shown once, when the key is made. QRX keeps only a hash of it, so a lost key cannot be recovered: revoke it and make another. - Keep keys on your server. Never put a key in a web page, an app bundle, a URL or a log. - You can have up to 10 active keys. Give each system its own key, so you can revoke one without touching the others. - A revoked key stops working at once. ## Environments {#environments} Keys are issued by the QRX site you make them on and work only there; keys on this site start with `qrx_live_`. ## Plans and limits {#limits} Codes made through the API count against the account’s plan exactly as codes made in the app do. The app and the API share one allowance. | | Free | Starter | | --- | --- | --- | | Codes per day | 42 | No daily limit | | Link codes | Yes | Yes | | Wi-Fi codes | No | Yes | | Change where a code points | No | Yes | | Scan analytics (in the app) | Totals only | Yes | - On the Free plan, every create says what is left today in the `"codes"` item of the `RateLimit` header (see [rate limits](#rate-limits)). `GET /v1/account` tells you the same at any time, with the time the count resets as `codes.resetsAt`. - When the day’s codes are used up, a create answers `429` with the `daily-limit` problem and `Retry-After` until the reset. Nothing is counted for a refused code. - Wi-Fi codes and changing a code’s destination on the Free plan answer `403` with `plan-required`. [Upgrade to Starter](https://qrx.codes/upgrade) to lift both. ## Rate limits {#rate-limits} Each key may make 30 requests per minute and create 6 codes per 10 seconds, on top of the plan’s allowance. Every response says where you stand: | Header | Meaning | | --- | --- | | `RateLimit-Policy` | The limits that apply, e.g. `"requests";q=30;w=60`: a quota `q` per window of `w` seconds | | `RateLimit` | Where you stand, e.g. `"requests";r=29;t=2`: `r` left, and `t` seconds that applies for, until more become available | | `Retry-After` | On a `429` or `409`, seconds to wait before trying again | The two `RateLimit` fields follow the IETF [RateLimit header fields draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/). On the Free plan a create adds a second policy, the day's codes: `RateLimit-Policy: "requests";q=30;w=60, "codes";q=42;w=86400` and `RateLimit: "requests";r=29;t=2, "codes";r=37;t=39345`. Read the items by name; more may be added. Going over answers `429` with the `rate-limited` problem. Wait for `Retry-After` seconds and try again. A request held open with `?wait=` (up to 50 seconds) counts once, so waiting is cheaper than polling. ## Errors {#errors} Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details with the `application/problem+json` type. `type` is stable, so match on it rather than on `detail`, which is written for people. Every response carries a `Request-Id` header, repeated in problems as `requestId`: quote it if you contact us. ```json 429 { "type": "https://qrx.codes/developers/docs/errors#daily-limit", "title": "Daily code limit reached", "status": 429, "detail": "No codes are left today. The Free plan resets at 2026-10-09T00:00:00.000Z; Starter has no daily limit.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` Every problem type, with when it happens and what to do, is listed on the [errors](https://qrx.codes/developers/docs/errors.md) page. ## Versioning {#versioning} The API is versioned in the path. Within `/v1`, changes only add: new endpoints, new optional fields in requests, new fields in responses, new values in enums (such as a code's `status`), new problem types and new headers. Ignore fields you do not recognise, and treat an enum value you do not know as the safe default its description gives: for `status`, anything other than `succeeded` or `failed` is still in progress. A change that could break a working integration would come as `/v2`, with notice. A link code's `id` is the path of its short link: lowercase letters and digits, currently 6 characters. A Wi-Fi code's `id` is a UUID. Treat every id as an opaque, case-sensitive string of up to 64 characters. It never changes. --- # API reference Codes are made asynchronously: creating one answers at once with its id and the status `processing`, and the code usually succeeds in under a minute. Poll it, or pass `?wait=45` to hold the request open until it finishes. Every code counts against the account’s plan exactly as codes made in the app do. Base URL: `https://qrx.codes/v1`. Authenticate every request with `Authorization: Bearer ` (or `X-API-Key: `). The machine-readable OpenAPI 3.1 document is at `https://qrx.codes/v1/openapi.json`. A `?wait=` holds a request open for up to 50 seconds. Send an `Idempotency-Key` with each create so a retry cannot make a second code. Errors are problem JSON, listed on the [errors](https://qrx.codes/developers/docs/errors.md) page. ## Headers {#headers} | Header | Meaning | | --- | --- | | `Request-Id` | This request’s id. Quote it to support. | | `RateLimit` | Where the credentials stand against each policy in `RateLimit-Policy` (IETF draft-ietf-httpapi-ratelimit-headers): `r` is what is left and `t` the seconds it applies for (until more is available), e.g. `"requests";r=29;t=2`. A create on a plan with a daily allowance adds a `"codes"` item: the codes left today and the seconds until the count resets. | | `RateLimit-Policy` | The limits that apply: `q` is the quota and `w` its window in seconds, e.g. `"requests";q=30;w=60`. A create on a plan with a daily allowance adds `"codes";q=42;w=86400`. More policies may be added. | | `Retry-After` | Seconds to wait before trying again. | | `Location` | The code’s absolute URL on the API. | | `Idempotent-Replayed` | `true` when this answers a retry of an earlier request with the same Idempotency-Key: the body is the code that request made, as it is now. | ## Create a code {#createCode} `POST /v1/codes` Starts a code from a prompt and a destination link (or Wi-Fi details, on Starter), or from your own picture instead of a prompt (`image` or `imageUrl`, or a `multipart/form-data` upload). Answers `202` at once with the code processing; with `?wait=`, answers when it finishes (`201`) or when the wait runs out (`202`). The `id` and `shortUrl` in the answer are the finished code’s and never change. Send an `Idempotency-Key` so a retry after a network failure cannot make a second code. Pass `callbackUrl` to be sent the code when it finishes instead of waiting, and `logo` (from `POST /v1/logos`) to place a logo in its centre. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `wait` | query | integer | No | Seconds to wait for the code to finish before answering, up to 50; 45 is a good value. 0 answers at once. 0 to 50. Default `0`. | | `Idempotency-Key` | header | string | No | A key of your choosing, unique per code you mean to create. A retry with the same key and body within a day answers with the code the first request made, as it is now, and never makes a second one. A retry that arrives before the first request has made its code answers `409` with `Retry-After`. 1 to 255 characters. | ### Request body A link code (with `destination`) or a Wi-Fi code (with `wifi`), painted from a `prompt` or drawn into your own picture (`image` or `imageUrl`). #### Link code A code that opens a web page. | Field | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | Yes | What the picture should show. 1 to 1000 characters. | | `destination` | string | Yes | Where scans go. An absolute `http://` or `https://` URL, stored as given after trimming. 1 to 2048 characters. | | `style` | string | No | A style id from `GET /v1/styles`. | | `logo` | string | No | The id of a logo from `POST /v1/logos`, placed in the centre of the code. Only the account that uploaded a logo can use it. | | `callbackUrl` | string | No | Where to `POST` the code (as `GET /v1/codes/{id}` shows it) once it has succeeded or failed, for automation platforms whose steps cannot wait a minute, such as Zapier. An `https` URL on the default port, on a host QRX delivers callbacks to (currently Zapier’s). Failed deliveries are retried with backoff, at most hourly, for 30 days; any `2xx` answer counts as delivered, and redirects are not followed. Retrying the create with the same `Idempotency-Key` sends the callback again once the code has finished. At most 2048 characters. | #### Wi-Fi code A code that joins a Wi-Fi network (Starter). | Field | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | Yes | What the picture should show. 1 to 1000 characters. | | `wifi` | WifiNetwork | Yes | | | `wifi.ssid` | string | Yes | The network’s name. 1 to 32 characters. | | `wifi.password` | string | Yes | 1 to 63 characters. | | `wifi.security` | one of `wpa`, `wep` | No | `wpa` or `wep`. `nopass`, for open networks, is planned. Default `"wpa"`. | | `style` | string | No | A style id from `GET /v1/styles`. | | `logo` | string | No | The id of a logo from `POST /v1/logos`, placed in the centre of the code. Only the account that uploaded a logo can use it. | | `callbackUrl` | string | No | Where to `POST` the code (as `GET /v1/codes/{id}` shows it) once it has succeeded or failed, for automation platforms whose steps cannot wait a minute, such as Zapier. An `https` URL on the default port, on a host QRX delivers callbacks to (currently Zapier’s). Failed deliveries are retried with backoff, at most hourly, for 30 days; any `2xx` answer counts as delivered, and redirects are not followed. Retrying the create with the same `Idempotency-Key` sends the callback again once the code has finished. At most 2048 characters. | #### Link code from your picture A code that opens a web page, drawn into your own picture. Send the picture as `image` or `imageUrl` (one of them), or as the file part `image` of a `multipart/form-data` body. Every picture, and any `prompt` with it, is checked for safety before anything is stored or drawn: one that does not pass is answered `422` with the problem type `unsafe-content`, and nothing is stored or counted. | Field | Type | Required | Description | | --- | --- | --- | --- | | `image` | string | No | Your own picture to draw the code into, base64 encoded or as a base64 `data:` URL: PNG, JPEG or WebP, at most 10 MB, at least 256 pixels on each side and no more than 4 times as wide as it is tall (or tall as wide). The code is drawn into its centre square. | | `imageUrl` | string | No | Or: a public `https` link to your picture, which QRX downloads (default port only, public addresses only, at most 3 redirects and 15 seconds): PNG, JPEG or WebP, at most 10 MB, at least 256 pixels on each side and no more than 4 times as wide as it is tall (or tall as wide). The code is drawn into its centre square. At most 2048 characters. | | `prompt` | string | No | Optional: what the picture shows, to guide how it is redrawn. Left out, the picture is described automatically. Checked for safety with the picture. At most 1000 characters. | | `destination` | string | Yes | Where scans go. An absolute `http://` or `https://` URL, stored as given after trimming. 1 to 2048 characters. | | `style` | string | No | A style id from `GET /v1/styles`. | | `logo` | string | No | The id of a logo from `POST /v1/logos`, placed in the centre of the code. Only the account that uploaded a logo can use it. | | `callbackUrl` | string | No | Where to `POST` the code (as `GET /v1/codes/{id}` shows it) once it has succeeded or failed, for automation platforms whose steps cannot wait a minute, such as Zapier. An `https` URL on the default port, on a host QRX delivers callbacks to (currently Zapier’s). Failed deliveries are retried with backoff, at most hourly, for 30 days; any `2xx` answer counts as delivered, and redirects are not followed. Retrying the create with the same `Idempotency-Key` sends the callback again once the code has finished. At most 2048 characters. | #### Wi-Fi code from your picture A code that joins a Wi-Fi network (Starter), drawn into your own picture. Send the picture as `image` or `imageUrl` (one of them), or as the file part `image` of a `multipart/form-data` body. Every picture, and any `prompt` with it, is checked for safety before anything is stored or drawn: one that does not pass is answered `422` with the problem type `unsafe-content`, and nothing is stored or counted. | Field | Type | Required | Description | | --- | --- | --- | --- | | `image` | string | No | Your own picture to draw the code into, base64 encoded or as a base64 `data:` URL: PNG, JPEG or WebP, at most 10 MB, at least 256 pixels on each side and no more than 4 times as wide as it is tall (or tall as wide). The code is drawn into its centre square. | | `imageUrl` | string | No | Or: a public `https` link to your picture, which QRX downloads (default port only, public addresses only, at most 3 redirects and 15 seconds): PNG, JPEG or WebP, at most 10 MB, at least 256 pixels on each side and no more than 4 times as wide as it is tall (or tall as wide). The code is drawn into its centre square. At most 2048 characters. | | `prompt` | string | No | Optional: what the picture shows, to guide how it is redrawn. Left out, the picture is described automatically. Checked for safety with the picture. At most 1000 characters. | | `wifi` | WifiNetwork | Yes | | | `wifi.ssid` | string | Yes | The network’s name. 1 to 32 characters. | | `wifi.password` | string | Yes | 1 to 63 characters. | | `wifi.security` | one of `wpa`, `wep` | No | `wpa` or `wep`. `nopass`, for open networks, is planned. Default `"wpa"`. | | `style` | string | No | A style id from `GET /v1/styles`. | | `logo` | string | No | The id of a logo from `POST /v1/logos`, placed in the centre of the code. Only the account that uploaded a logo can use it. | | `callbackUrl` | string | No | Where to `POST` the code (as `GET /v1/codes/{id}` shows it) once it has succeeded or failed, for automation platforms whose steps cannot wait a minute, such as Zapier. An `https` URL on the default port, on a host QRX delivers callbacks to (currently Zapier’s). Failed deliveries are retried with backoff, at most hourly, for 30 days; any `2xx` answer counts as delivered, and redirects are not followed. Retrying the create with the same `Idempotency-Key` sends the callback again once the code has finished. At most 2048 characters. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `201` | The code finished within the wait. | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` | | `202` | The code has started and is processing. | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | Wi-Fi codes need the Starter plan, or the account is gone. Problems: [`plan-required`](https://qrx.codes/developers/docs/errors.md#plan-required), [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `409` | A request with this Idempotency-Key is still running. Problems: [`idempotency-in-progress`](https://qrx.codes/developers/docs/errors.md#idempotency-in-progress). | `Request-Id`, `Retry-After` | | `413` | The body is larger than 14 MB, or the picture is larger than 10 MB. Problems: [`payload-too-large`](https://qrx.codes/developers/docs/errors.md#payload-too-large). | `Request-Id` | | `422` | The picture or its description did not pass the safety check (nothing was stored or counted), the code could not be started (for example, the logo was not found), or the Idempotency-Key was used for a different request. Problems: [`unsafe-content`](https://qrx.codes/developers/docs/errors.md#unsafe-content), [`unprocessable`](https://qrx.codes/developers/docs/errors.md#unprocessable), [`idempotency-mismatch`](https://qrx.codes/developers/docs/errors.md#idempotency-mismatch). | `Request-Id` | | `429` | A rate limit or the Free plan’s daily allowance is used up. `Retry-After` says when to try again. With `daily-limit`, `RateLimit` carries the `"codes"` item. Problems: [`daily-limit`](https://qrx.codes/developers/docs/errors.md#daily-limit), [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl -X POST https://qrx.codes/v1/codes \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Idempotency-Key: menu-2026-10-08" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A lighthouse on a cliff at sunset", "destination": "https://example.com/menu" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'menu-2026-10-08', }, body: JSON.stringify({ prompt: 'A lighthouse on a cliff at sunset', destination: 'https://example.com/menu', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/codes", headers={"Authorization": f"Bearer {QRX_API_KEY}", "Idempotency-Key": "menu-2026-10-08"}, json={ "prompt": "A lighthouse on a cliff at sunset", "destination": "https://example.com/menu", }, ) data = response.json() ``` ### Example responses ```json 201 { "id": "k7m2qz", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": "https://qrx.codes/media/premium/k7m2qz.png", "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": "2026-10-08T01:24:31.000Z", "derivedFrom": null } ``` ```json 202 { "id": "k7m2qz", "object": "code", "status": "processing", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": null, "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": null, "derivedFrom": null } ``` ## List codes {#listCodes} `GET /v1/codes` The account’s codes, newest first, a page at a time. Pass `nextCursor` as `cursor` for the next page. Filtered to finished codes (`status=succeeded` or `failed`, or `completedAfter`), the list is in order of `completedAt`, newest first, so polling with `status=succeeded&completedAfter=` sees every code that has finished since (drop ids already seen), however long each took. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | No | How many items to return. 1 to 100. Default `20`. | | `cursor` | query | string | No | `nextCursor` from the previous page of the same list (the same filters). | | `status` | query | one of `processing`, `succeeded`, `failed` | No | Only codes with this status. `succeeded` or `failed` lists by `completedAt`, newest first. | | `completedAfter` | query | string | No | Only codes that finished at or after this time (ISO 8601 with a time zone, e.g. `2026-10-10T09:00:00Z`), listed by `completedAt`, newest first. Inclusive, so codes that finished in the same millisecond as the newest `completedAt` you have seen are listed again rather than missed: drop the ids you already have. Not with `status=processing`. ISO 8601 date and time. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `200` | A page of codes. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl "https://qrx.codes/v1/codes?limit=10" \ -H "Authorization: Bearer $QRX_API_KEY" ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes?limit=10', { headers: { Authorization: `Bearer ${QRX_API_KEY}`, }, }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.get( "https://qrx.codes/v1/codes", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, params={ "limit": 10, }, ) data = response.json() ``` ### Example response ```json 200 { "object": "list", "data": [ { "id": "k7m2qz", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": "https://qrx.codes/media/premium/k7m2qz.png", "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": "2026-10-08T01:24:31.000Z", "derivedFrom": null } ], "hasMore": false, "nextCursor": null } ``` ## Get a code {#getCode} `GET /v1/codes/{id}` A code’s status and, once it has succeeded, its image. With `?wait=`, answers as soon as the code finishes or when the wait runs out, whichever is first, so one request replaces many polls. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | The code’s id, as returned when it was created. At most 64 characters. | | `wait` | query | integer | No | Seconds to wait for the code to finish before answering, up to 50; 45 is a good value. 0 answers at once. 0 to 50. Default `0`. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `200` | The code. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `404` | The account has no code with that id. Problems: [`not-found`](https://qrx.codes/developers/docs/errors.md#not-found). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl "https://qrx.codes/v1/codes/k7m2qz?wait=45" \ -H "Authorization: Bearer $QRX_API_KEY" ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes/k7m2qz?wait=45', { headers: { Authorization: `Bearer ${QRX_API_KEY}`, }, }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.get( "https://qrx.codes/v1/codes/k7m2qz", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, params={ "wait": 45, }, ) data = response.json() ``` ### Example response ```json 200 { "id": "k7m2qz", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": "https://qrx.codes/media/premium/k7m2qz.png", "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": "2026-10-08T01:24:31.000Z", "derivedFrom": null } ``` ## Change where a code points {#updateCode} `PATCH /v1/codes/{id}` Points a link code somewhere else; printed codes follow at once. Needs the Starter plan. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | The code’s id, as returned when it was created. At most 64 characters. | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `destination` | string | Yes | Where scans go from now on. An absolute `http://` or `https://` URL, stored as given after trimming. 1 to 2048 characters. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `200` | The updated code. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | Changing where a code points needs the Starter plan, or the account is gone. Problems: [`plan-required`](https://qrx.codes/developers/docs/errors.md#plan-required), [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `404` | The account has no code with that id. Problems: [`not-found`](https://qrx.codes/developers/docs/errors.md#not-found). | `Request-Id` | | `422` | Wi-Fi codes cannot change. Problems: [`unprocessable`](https://qrx.codes/developers/docs/errors.md#unprocessable). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl -X PATCH https://qrx.codes/v1/codes/k7m2qz \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "https://example.com/new-menu" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes/k7m2qz', { method: 'PATCH', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ destination: 'https://example.com/new-menu', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.patch( "https://qrx.codes/v1/codes/k7m2qz", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, json={ "destination": "https://example.com/new-menu", }, ) data = response.json() ``` ### Example response ```json 200 { "id": "k7m2qz", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset", "style": null, "destination": "https://example.com/new-menu", "wifi": null, "shortUrl": "https://qrx.to/k7m2qz", "imageUrl": "https://qrx.codes/media/premium/k7m2qz.png", "pageUrl": "https://qrx.codes/view/k7m2qz", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": "2026-10-08T01:24:31.000Z", "derivedFrom": null } ``` ## Create a variation of a code {#createVariation} `POST /v1/codes/{id}/variations` Starts a new code from one of the account’s link codes with a change to its picture, e.g. `{ "change": "more trees" }`. The variation is a code of its own, with its own `id` and `shortUrl`, that opens the same `destination` and keeps the original’s style and logo; the original never changes. Its `derivedFrom` names the original and the change. Codes made since QRX keeps each picture are varied by changing that picture (`derivedFrom.method` `edit`); older codes are drawn again from their prompt with the change added (`redraw`), so their composition can differ more. It counts against the plan like any code and answers like `POST /v1/codes`: `202` at once, or `201` once it finishes within `?wait=`. Send an `Idempotency-Key`. The original must have succeeded; a code the account does not own is not found. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | The code’s id, as returned when it was created. At most 64 characters. | | `wait` | query | integer | No | Seconds to wait for the code to finish before answering, up to 50; 45 is a good value. 0 answers at once. 0 to 50. Default `0`. | | `Idempotency-Key` | header | string | No | A key of your choosing, unique per code you mean to create. A retry with the same key and body within a day answers with the code the first request made, as it is now, and never makes a second one. A retry that arrives before the first request has made its code answers `409` with `Retry-After`. 1 to 255 characters. | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `change` | string | Yes | What to change in the picture, in words, e.g. `more trees` or `make it night time`. 1 to 500 characters. | | `callbackUrl` | string | No | Where to `POST` the code (as `GET /v1/codes/{id}` shows it) once it has succeeded or failed, for automation platforms whose steps cannot wait a minute, such as Zapier. An `https` URL on the default port, on a host QRX delivers callbacks to (currently Zapier’s). Failed deliveries are retried with backoff, at most hourly, for 30 days; any `2xx` answer counts as delivered, and redirects are not followed. Retrying the create with the same `Idempotency-Key` sends the callback again once the code has finished. At most 2048 characters. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `201` | The variation finished within the wait. | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` | | `202` | The variation has started and is processing. | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `404` | The account has no code with that id. Problems: [`not-found`](https://qrx.codes/developers/docs/errors.md#not-found). | `Request-Id` | | `409` | A request with this Idempotency-Key is still running. Problems: [`idempotency-in-progress`](https://qrx.codes/developers/docs/errors.md#idempotency-in-progress). | `Request-Id`, `Retry-After` | | `413` | The body is larger than 16 KB. Problems: [`payload-too-large`](https://qrx.codes/developers/docs/errors.md#payload-too-large). | `Request-Id` | | `422` | The code cannot be varied (it is a Wi-Fi code, or it has not succeeded), or the Idempotency-Key was used for a different request. Problems: [`unprocessable`](https://qrx.codes/developers/docs/errors.md#unprocessable), [`idempotency-mismatch`](https://qrx.codes/developers/docs/errors.md#idempotency-mismatch). | `Request-Id` | | `429` | A rate limit or the Free plan’s daily allowance is used up. `Retry-After` says when to try again. With `daily-limit`, `RateLimit` carries the `"codes"` item. Problems: [`daily-limit`](https://qrx.codes/developers/docs/errors.md#daily-limit), [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl -X POST https://qrx.codes/v1/codes/k7m2qz/variations \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Idempotency-Key: lighthouse-storm-1" \ -H "Content-Type: application/json" \ -d '{ "change": "add storm clouds and crashing waves" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes/k7m2qz/variations', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'lighthouse-storm-1', }, body: JSON.stringify({ change: 'add storm clouds and crashing waves', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/codes/k7m2qz/variations", headers={"Authorization": f"Bearer {QRX_API_KEY}", "Idempotency-Key": "lighthouse-storm-1"}, json={ "change": "add storm clouds and crashing waves", }, ) data = response.json() ``` ### Example responses ```json 201 { "id": "p3w8vn", "object": "code", "status": "succeeded", "type": "url", "prompt": "A lighthouse on a cliff at sunset, add storm clouds and crashing waves", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/p3w8vn", "imageUrl": "https://qrx.codes/media/premium/p3w8vn.png", "pageUrl": "https://qrx.codes/view/p3w8vn", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": "2026-10-08T01:24:31.000Z", "derivedFrom": { "id": "k7m2qz", "change": "add storm clouds and crashing waves", "method": "edit" } } ``` ```json 202 { "id": "p3w8vn", "object": "code", "status": "processing", "type": "url", "prompt": "A lighthouse on a cliff at sunset, add storm clouds and crashing waves", "style": null, "destination": "https://example.com/menu", "wifi": null, "shortUrl": "https://qrx.to/p3w8vn", "imageUrl": null, "pageUrl": "https://qrx.codes/view/p3w8vn", "error": null, "createdAt": "2026-10-08T01:23:45.000Z", "completedAt": null, "derivedFrom": { "id": "k7m2qz", "change": "add storm clouds and crashing waves", "method": "edit" } } ``` ## Report a code’s image {#reportCode} `POST /v1/codes/{id}/report` Reports a code’s picture as offensive or otherwise inappropriate, for QRX to review, and takes it out of the public gallery. The body is optional, and reporting a code again is fine. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | The code’s id, as returned when it was created. At most 64 characters. | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | No | What is wrong with the picture, in the reporter’s words. Trimmed; optional. At most 500 characters. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `202` | The report has been received. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `404` | The account has no code with that id. Problems: [`not-found`](https://qrx.codes/developers/docs/errors.md#not-found). | `Request-Id` | | `413` | The body is larger than 16 KB. Problems: [`payload-too-large`](https://qrx.codes/developers/docs/errors.md#payload-too-large). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl -X POST https://qrx.codes/v1/codes/k7m2qz/report \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "The picture shows something offensive." }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/codes/k7m2qz/report', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ reason: 'The picture shows something offensive.', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/codes/k7m2qz/report", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, json={ "reason": "The picture shows something offensive.", }, ) data = response.json() ``` ### Example response ```json 202 { "object": "report", "code": "k7m2qz", "receivedAt": "2026-10-08T01:23:45.000Z" } ``` ## Upload a logo {#uploadLogo} `POST /v1/logos` Stores a logo to place in the centre of codes: pass the `id` it answers with as `logo` when creating a code, as often as you like. A PNG, JPEG or WebP image of up to 10 MB, base64 encoded or as a `data:` URL; it is stored as a PNG of at most 1024 px on its longer side, keeping transparency. SVGs are not accepted: send them rasterised. Uploading needs codes left today. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `image` | string | Yes | A PNG, JPEG or WebP image of up to 10 MB, base64 encoded, or as a `data:image/…;base64,` URL. | ### Responses | Status | Description | Headers | | --- | --- | --- | | `201` | The logo is stored. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `400` | The request is not valid; `errors` lists each field. Problems: [`invalid-request`](https://qrx.codes/developers/docs/errors.md#invalid-request). | `Request-Id` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `413` | The logo is larger than 10 MB. Problems: [`payload-too-large`](https://qrx.codes/developers/docs/errors.md#payload-too-large). | `Request-Id` | | `422` | The image is not a PNG, JPEG or WebP image. Problems: [`unprocessable`](https://qrx.codes/developers/docs/errors.md#unprocessable). | `Request-Id` | | `429` | A rate limit or the Free plan’s daily allowance is used up. `Retry-After` says when to try again. With `daily-limit`, `RateLimit` carries the `"codes"` item. Problems: [`daily-limit`](https://qrx.codes/developers/docs/errors.md#daily-limit), [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl -X POST https://qrx.codes/v1/logos \ -H "Authorization: Bearer $QRX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" }' ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/logos', { method: 'POST', headers: { Authorization: `Bearer ${QRX_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', }), }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.post( "https://qrx.codes/v1/logos", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, json={ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", }, ) data = response.json() ``` ### Example response ```json 201 { "id": "3f2b8c1e-7d4a-4f6b-9c2e-5a1d8e7f3b20", "object": "logo", "createdAt": "2026-10-08T01:23:40.000Z" } ``` ## Get the account {#getAccount} `GET /v1/account` The plan, the codes left today and when the count resets, the rate limits, and the credentials in use. ### Responses | Status | Description | Headers | | --- | --- | --- | | `200` | The account. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl https://qrx.codes/v1/account \ -H "Authorization: Bearer $QRX_API_KEY" ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/account', { headers: { Authorization: `Bearer ${QRX_API_KEY}`, }, }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.get( "https://qrx.codes/v1/account", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, ) data = response.json() ``` ### Example response ```json 200 { "object": "account", "plan": "free", "codes": { "dailyLimit": 42, "remainingToday": 38, "resetsAt": "2026-10-09T00:00:00.000Z" }, "features": { "wifiCodes": false, "changeDestination": false }, "rateLimits": { "requests": { "limit": 30, "windowSeconds": 60 }, "codes": { "limit": 6, "windowSeconds": 10 } }, "credentials": { "type": "api_key", "prefix": "qrx_live_abc123def456", "environment": "live" } } ``` ## List styles {#listStyles} `GET /v1/styles` Styles a code can be painted in; pass an id as `style` when creating a code. ### Responses | Status | Description | Headers | | --- | --- | --- | | `200` | Every style. | `Request-Id`, `RateLimit`, `RateLimit-Policy` | | `401` | The credentials are missing, invalid or revoked. Problems: [`unauthorized`](https://qrx.codes/developers/docs/errors.md#unauthorized). | `Request-Id`, `WWW-Authenticate` | | `403` | The account the credentials belong to is gone. Problems: [`forbidden`](https://qrx.codes/developers/docs/errors.md#forbidden). | `Request-Id` | | `429` | The key has made too many requests in a short time. `Retry-After` says when to try again. Problems: [`rate-limited`](https://qrx.codes/developers/docs/errors.md#rate-limited). | `Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` | | `500` | Something failed on our side. Problems: [`internal`](https://qrx.codes/developers/docs/errors.md#internal). | `Request-Id` | | `503` | A service QRX relies on, such as the one whose keys check OAuth access tokens, did not answer. The credentials are still good; retry after `Retry-After` seconds. Problems: [`unavailable`](https://qrx.codes/developers/docs/errors.md#unavailable). | `Request-Id`, `Retry-After` | ### Example request ```bash curl curl https://qrx.codes/v1/styles \ -H "Authorization: Bearer $QRX_API_KEY" ``` ```javascript JavaScript const QRX_API_KEY = process.env.QRX_API_KEY; const response = await fetch('https://qrx.codes/v1/styles', { headers: { Authorization: `Bearer ${QRX_API_KEY}`, }, }); const data = await response.json(); ``` ```python Python import os import requests QRX_API_KEY = os.environ["QRX_API_KEY"] response = requests.get( "https://qrx.codes/v1/styles", headers={"Authorization": f"Bearer {QRX_API_KEY}"}, ) data = response.json() ``` ### Example response ```json 200 { "object": "list", "data": [ { "id": "a64c5", "object": "style", "name": "Anime", "previewUrl": "https://qrx.codes/img/styles/anime-192.webp" }, { "id": "06184", "object": "style", "name": "Cartoon", "previewUrl": "https://qrx.codes/img/styles/cartoon-192.webp" } ], "hasMore": false, "nextCursor": null } ``` ## Objects {#objects} ### Code {#schema-code} | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | The code’s id. A link code’s id is also the path of its short link: lowercase letters and digits, currently 6 characters. A Wi-Fi code’s id is a UUID. Treat every id as an opaque, case-sensitive string of up to 64 characters. It never changes. At most 64 characters. | | `object` | `"code"` | Yes | | | `status` | one of `queued`, `processing`, `succeeded`, `failed` (more may be added) | Yes | `processing` until the picture is painted and checked to scan; then `succeeded` or `failed`. `queued` is reserved for a code waiting to start. More values may be added: treat anything other than `succeeded` or `failed` as still in progress. | | `type` | one of `url`, `wifi` (more may be added) | Yes | `url` for a link code, `wifi` for a Wi-Fi code. More values may be added. | | `prompt` | string | Yes | | | `style` | string or null | Yes | The style id, if one was chosen. | | `destination` | string or null | Yes | Where a link code’s scans go; null for a Wi-Fi code. | | `wifi` | object or null | Yes | The network a Wi-Fi code joins; null for a link code. The password is never returned. | | `wifi.ssid` | string | Yes | | | `wifi.security` | one of `wpa`, `wep` (more may be added) | Yes | More values (such as `nopass`) may be added. | | `shortUrl` | string or null | Yes | The link the code encodes (`https://qrx.to/`); scans go through it to `destination`. Null for a Wi-Fi code. | | `imageUrl` | string or null | Yes | The finished image (PNG), once the code has succeeded. | | `pageUrl` | string | Yes | The code’s page in the app. | | `error` | object or null | Yes | Why the code failed, when it did. | | `error.message` | string | Yes | | | `createdAt` | string | Yes | ISO 8601 date and time. | | `completedAt` | string or null | Yes | When the code succeeded or failed; null while it is processing. ISO 8601 date and time. | | `derivedFrom` | object or null | Yes | Set on a variation (`POST /v1/codes/{id}/variations`): the code it was made from and the change; null otherwise. | | `derivedFrom.id` | string | Yes | The id of the code it was made from. At most 64 characters. | | `derivedFrom.change` | string | Yes | The change that was asked for. | | `derivedFrom.method` | one of `edit`, `redraw` (more may be added) | Yes | `edit`: the original’s picture was changed. `redraw`: the original was made before QRX kept pictures, so the variation was drawn again from its prompt with the change. More values may be added. | ### CodeList {#schema-codelist} | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | `"list"` | Yes | | | `data` | array of Code | Yes | | | `hasMore` | boolean | Yes | | | `nextCursor` | string or null | Yes | Pass as `cursor` for the next page; null on the last page. | ### Style {#schema-style} | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | | | `object` | `"style"` | Yes | | | `name` | string | Yes | | | `previewUrl` | string or null | Yes | A picture in the style. | ### StyleList {#schema-stylelist} | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | `"list"` | Yes | | | `data` | array of Style | Yes | | | `hasMore` | boolean | Yes | | | `nextCursor` | null | Yes | | ### RateLimit {#schema-ratelimit} | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | Yes | | | `windowSeconds` | integer | Yes | | ### Account {#schema-account} | Field | Type | Required | Description | | --- | --- | --- | --- | | `object` | `"account"` | Yes | | | `plan` | one of `free`, `starter` (more may be added) | Yes | What the account pays for. `features` says what it can do today, and the two can differ (for QRX staff, say), so act on `features`. More values may be added. | | `codes` | object | Yes | The daily allowance. All null when the account has no daily limit. | | `codes.dailyLimit` | integer or null | Yes | | | `codes.remainingToday` | integer or null | Yes | | | `codes.resetsAt` | string or null | Yes | ISO 8601 date and time. | | `features` | object | Yes | | | `features.wifiCodes` | boolean | Yes | | | `features.changeDestination` | boolean | Yes | | | `rateLimits` | object | Yes | | | `rateLimits.requests` | RateLimit | Yes | | | `rateLimits.requests.limit` | integer | Yes | | | `rateLimits.requests.windowSeconds` | integer | Yes | | | `rateLimits.codes` | RateLimit | Yes | | | `rateLimits.codes.limit` | integer | Yes | | | `rateLimits.codes.windowSeconds` | integer | Yes | | | `credentials` | object | Yes | | | `credentials.type` | one of `api_key`, `oauth` (more may be added) | Yes | More values may be added. | | `credentials.prefix` | string or null | Yes | The start of the API key, enough to tell keys apart. | | `credentials.environment` | one of `test`, `live` (more may be added) | Yes | The QRX site the credentials work on: `live` on qrx.codes. More values may be added. | ### Problem {#schema-problem} RFC 9457 problem details. | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string | Yes | A stable URI naming the problem: match on this. Each one is listed in `x-problem-types` and on the errors page; more may be added. | | `title` | string | Yes | | | `status` | integer | Yes | | | `detail` | string | Yes | What went wrong, for people. It can change. | | `requestId` | string | No | The request’s id, also in the `Request-Id` header. | | `errors` | array of object | No | On `invalid-request`: each field that is wrong (dotted paths such as `wifi.ssid`), and why. | --- # Errors When a request fails, the API answers with an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem: a JSON body with the `application/problem+json` type. | Field | Meaning | | --- | --- | | `type` | A stable URI naming the problem. Match on this. | | `title` | A short summary of the problem type | | `status` | The HTTP status, repeated in the body | | `detail` | What went wrong with this request, written for people. It can change; do not match on it. | | `errors` | On `invalid-request` only: each field that is wrong, and why | | `requestId` | The request’s id, also in the `Request-Id` header. Quote it if you contact us. | A `429` and a `409` carry `Retry-After`, in seconds. Retry only the problems marked safe to retry, and back off: wait at least `Retry-After`, and longer each time it repeats. | Status | Type | Title | | --- | --- | --- | | `401` | [`unauthorized`](#unauthorized) | Missing or invalid credentials | | `403` | [`forbidden`](#forbidden) | Not allowed | | `403` | [`plan-required`](#plan-required) | Needs the Starter plan | | `404` | [`not-found`](#not-found) | Not found | | `400` | [`invalid-request`](#invalid-request) | Invalid request | | `413` | [`payload-too-large`](#payload-too-large) | Request body too large | | `422` | [`unprocessable`](#unprocessable) | Request could not be completed | | `422` | [`unsafe-content`](#unsafe-content) | Content not accepted | | `422` | [`idempotency-mismatch`](#idempotency-mismatch) | Idempotency key reused with a different request | | `409` | [`idempotency-in-progress`](#idempotency-in-progress) | A request with this idempotency key is in progress | | `429` | [`daily-limit`](#daily-limit) | Daily code limit reached | | `429` | [`rate-limited`](#rate-limited) | Too many requests | | `500` | [`internal`](#internal) | Something went wrong | | `503` | [`unavailable`](#unavailable) | Temporarily unavailable | ## Missing or invalid credentials {#unauthorized} `401` · `unauthorized` **When:** No credentials were sent, or they are not a QRX key or token, belong to another QRX site, or the key has been revoked. **What to do:** Send `Authorization: Bearer ` (or `X-API-Key: `) with an active key from the API keys page. A key works only on the QRX site that made it. ```json 401 { "type": "https://qrx.codes/developers/docs/errors#unauthorized", "title": "Missing or invalid credentials", "status": 401, "detail": "That API key has been revoked.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Not allowed {#forbidden} `403` · `forbidden` **When:** The account the credentials belong to no longer exists. **What to do:** Make a key from an active account. ```json 403 { "type": "https://qrx.codes/developers/docs/errors#forbidden", "title": "Not allowed", "status": 403, "detail": "The account these credentials belong to no longer exists.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Needs the Starter plan {#plan-required} `403` · `plan-required` **When:** The request needs the Starter plan: Wi-Fi codes, or changing where a code points. **What to do:** Upgrade the account to Starter, or make a link code instead. ```json 403 { "type": "https://qrx.codes/developers/docs/errors#plan-required", "title": "Needs the Starter plan", "status": 403, "detail": "Wi-Fi codes need the Starter plan.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Not found {#not-found} `404` · `not-found` **When:** There is no such endpoint, or the account has no code with that id. **What to do:** Check the method and path, and use an id returned when the code was created. ```json 404 { "type": "https://qrx.codes/developers/docs/errors#not-found", "title": "Not found", "status": 404, "detail": "You have no code k7m2qz.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Invalid request {#invalid-request} `400` · `invalid-request` **When:** The body, query or a header is not valid, for example a `destination` that is not an absolute `http` or `https` URL. `errors` lists each field and what is wrong with it. **What to do:** Correct the fields named in `errors` and send the request again. ```json 400 { "type": "https://qrx.codes/developers/docs/errors#invalid-request", "title": "Invalid request", "status": 400, "detail": "prompt is required", "errors": [ { "field": "prompt", "message": "is required" } ], "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Request body too large {#payload-too-large} `413` · `payload-too-large` **When:** The body is larger than 16 KB (14 MB with a picture or logo), or a picture or logo is larger than 10 MB. **What to do:** Shorten the prompt (at most 1,000 characters), or send a smaller picture or logo. ```json 413 { "type": "https://qrx.codes/developers/docs/errors#payload-too-large", "title": "Request body too large", "status": 413, "detail": "The body is larger than 16 KB.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Request could not be completed {#unprocessable} `422` · `unprocessable` **When:** The request is valid but could not be carried out, for example a prompt our content filter refuses, or a Wi-Fi code asked to change its destination. **What to do:** Read `detail`, change the request and try again. ```json 422 { "type": "https://qrx.codes/developers/docs/errors#unprocessable", "title": "Request could not be completed", "status": 422, "detail": "Only link codes can change where they point.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Content not accepted {#unsafe-content} `422` · `unsafe-content` **When:** A picture sent to make a code from (`image` or `imageUrl`), or the `prompt` sent with it, did not pass our safety check. Nothing was stored, drawn or counted. **What to do:** Send a different picture or description. The reasons are not given. ```json 422 { "type": "https://qrx.codes/developers/docs/errors#unsafe-content", "title": "Content not accepted", "status": 422, "detail": "We can't process this image for safety reasons. Choose a different image.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Idempotency key reused with a different request {#idempotency-mismatch} `422` · `idempotency-mismatch` **When:** An `Idempotency-Key` was sent again with a different request body within a day. **What to do:** Use a new key for each new request; reuse a key only to retry exactly the same request. ```json 422 { "type": "https://qrx.codes/developers/docs/errors#idempotency-mismatch", "title": "Idempotency key reused with a different request", "status": 422, "detail": "This Idempotency-Key was used for a different request. Use a new key for a new request.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## A request with this idempotency key is in progress {#idempotency-in-progress} `409` · `idempotency-in-progress` · safe to retry **When:** A request with the same `Idempotency-Key` has not made its code yet. Once it has, a retry answers with that code instead. **What to do:** Wait for `Retry-After` seconds and retry with the same key; you then get the code the first request made. ```json 409 { "type": "https://qrx.codes/developers/docs/errors#idempotency-in-progress", "title": "A request with this idempotency key is in progress", "status": 409, "detail": "A request with this Idempotency-Key is still running. Retry shortly.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Daily code limit reached {#daily-limit} `429` · `daily-limit` · safe to retry **When:** The Free plan’s daily allowance is used up. Codes made in the app count too. **What to do:** Wait for `Retry-After` seconds (the `"codes"` item of `RateLimit` says the same), or upgrade to Starter, which has no daily limit. ```json 429 { "type": "https://qrx.codes/developers/docs/errors#daily-limit", "title": "Daily code limit reached", "status": 429, "detail": "No codes are left today. The Free plan resets at 2026-10-09T00:00:00.000Z; Starter has no daily limit.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Too many requests {#rate-limited} `429` · `rate-limited` · safe to retry **When:** The credentials have made too many requests, or started too many codes, in a short time. **What to do:** Wait for `Retry-After` seconds before trying again. Wait for a code with `?wait=` rather than polling quickly. ```json 429 { "type": "https://qrx.codes/developers/docs/errors#rate-limited", "title": "Too many requests", "status": 429, "detail": "Too many requests. Try again in 4 seconds.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Something went wrong {#internal} `500` · `internal` · safe to retry **When:** Something failed on our side. **What to do:** Try again shortly. If it keeps happening, email hello@qrx.codes with the `requestId`. ```json 500 { "type": "https://qrx.codes/developers/docs/errors#internal", "title": "Something went wrong", "status": 500, "detail": "The request failed on our side. Try again shortly.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` ## Temporarily unavailable {#unavailable} `503` · `unavailable` · safe to retry **When:** A service QRX relies on did not answer, such as the sign-in service whose keys check OAuth access tokens, or the safety check every picture goes through. **What to do:** Your credentials are still good: keep them, and try again after `Retry-After` seconds. ```json 503 { "type": "https://qrx.codes/developers/docs/errors#unavailable", "title": "Temporarily unavailable", "status": 503, "detail": "Access tokens cannot be checked right now. Try again in 5 seconds; the token is still good.", "requestId": "req_8mGvXbQ2nT5wK1pL9sD3" } ``` Questions about an error? Email hello@qrx.codes, or see the [overview](https://qrx.codes/developers.md) for how limits work. --- # AI agents and MCP QRX works with AI agents in two ways. An **MCP server** lets assistants such as Claude, ChatGPT and Cursor make and read QR codes for you. **Agent-ready docs** let a coding agent read the API and write an integration with you. ## MCP server {#mcp-server} ```text MCP server URL https://qrx.codes/mcp ``` The server speaks Streamable HTTP. Add the URL to your client and sign in to QRX when it asks: the client sends you to a QRX sign-in page to approve access, with no key to copy. For scripts, CI and clients that can't sign in, use one of your [API keys](https://qrx.codes/developers/keys) instead, sent as `Authorization: Bearer `, or as `X-API-Key: ` where a client can only add a plain header. Codes made through it count against your plan and its rate limits, exactly like the REST API and the app. A key's usage shows on its [usage](https://qrx.codes/developers/usage) page. The [MCP server page](https://qrx.codes/developers/mcp) has the same set-up for every client, and what the server sees of your conversations. | Tool | | What it is for | | --- | --- | --- | | `generate_qr_code` | Writes | The user wants a new QR code that is also a picture: a scannable code painted into art from a description, that opens a link | | `generate_qr_code_from_image` | Writes | The user wants a scannable QR code woven into a picture they already have (a photo, an illustration or artwork) instead of one painted from a description | | `vary_qr_code` | Writes | The user wants a new QR code based on one of their existing codes, with a change to its picture, e.g | | `get_qr_code` | Reads | You have a QR code’s id (from generate_qr_code, vary_qr_code or list_qr_codes) and need its status or finished image | | `list_qr_codes` | Reads | The user asks about QR codes they already have, newest first, a page at a time (pass nextCursor as cursor for the next page) | | `change_qr_code_destination` | Writes | The user wants an existing link QR code to open a different web address | | `get_account` | Reads | The user asks how many QR codes they can still make today, when the daily count resets, or what their QRX account can do | | `get_profile` | Reads | You need to know which QRX account is connected, for example to confirm it with the user | | `list_styles` | Reads | The user wants to choose a look for a new QR code; pass a style id to generate_qr_code | A new code usually takes under a minute to paint. `generate_qr_code` answers at once with the code's id, and `get_qr_code` waits up to 25 seconds for it at a time. `vary_qr_code` makes a new code from one of yours with a change to its picture ("more trees", "make it night time"): the new code has its own id and short link to the same destination, the original does not change, and you wait for it with `get_qr_code` too. In apps that show interactive views, such as Claude and ChatGPT, the code appears in a card that fills in when it is ready. To weave a code into a picture the user already has, `generate_qr_code_from_image` takes the picture instead of a prompt. In ChatGPT, attach the picture to the message and ChatGPT passes it to the tool. In Claude and most other clients, give a public `https` link to the picture (`imageUrl`); scripts and agents that hold the file can send it as base64 (`imageBase64`). PNG, JPEG or WebP, at most 10 MB and at least 256 pixels on each side. Every picture is checked for safety first, and one that does not pass comes back as an error result saying so, with nothing made or counted. > **Tip:** Make a separate key for each agent, so you can see its usage and revoke it on its own. ## Connect your client {#connect} Add the server to your client and sign in to QRX when it asks. ```bash Claude Code claude mcp add --transport http --scope user qrx https://qrx.codes/mcp ``` ```json Cursor { "mcpServers": { "qrx": { "url": "https://qrx.codes/mcp" } } } ``` ```json VS Code { "servers": { "qrx": { "type": "http", "url": "https://qrx.codes/mcp" } } } ``` ```json Windsurf { "mcpServers": { "qrx": { "serverUrl": "https://qrx.codes/mcp" } } } ``` ```json Cline { "mcpServers": { "qrx": { "type": "streamableHttp", "url": "https://qrx.codes/mcp", "disabled": false, "autoApprove": [ "get_qr_code", "list_qr_codes", "get_account", "get_profile", "list_styles" ] } } } ``` To use an API key instead, put it in the `QRX_API_KEY` environment variable first. A configured `Authorization` header replaces sign-in. ```bash Claude Code claude mcp add --transport http --scope project qrx https://qrx.codes/mcp \ --header 'Authorization: Bearer ${QRX_API_KEY}' ``` ```json Cursor { "mcpServers": { "qrx": { "url": "https://qrx.codes/mcp", "headers": { "Authorization": "Bearer ${env:QRX_API_KEY}" } } } } ``` ```json VS Code { "inputs": [ { "type": "promptString", "id": "qrx-api-key", "description": "QRX API key", "password": true } ], "servers": { "qrx": { "type": "http", "url": "https://qrx.codes/mcp", "headers": { "Authorization": "Bearer ${input:qrx-api-key}" } } } } ``` ```json Windsurf { "mcpServers": { "qrx": { "serverUrl": "https://qrx.codes/mcp", "headers": { "Authorization": "Bearer ${env:QRX_API_KEY}" } } } } ``` ```json Cline { "mcpServers": { "qrx": { "type": "streamableHttp", "url": "https://qrx.codes/mcp", "headers": { "Authorization": "Bearer qrx_live_…" }, "disabled": false, "autoApprove": [ "get_qr_code", "list_qr_codes", "get_account", "get_profile", "list_styles" ] } } } ``` ```json Local (stdio) { "mcpServers": { "qrx": { "command": "npx", "args": [ "-y", "@qrxcodes/mcp" ], "env": { "QRX_API_KEY": "qrx_live_…" } } } } ``` - **Claude (claude.ai and Claude Desktop):** add a custom connector. Open **Customize → Connectors**, select **Add custom connector**, name it QRX and give the server URL. Sign in to QRX when Claude asks. To use a key instead, choose **No sign-in** and add the request header `Authorization` with `Bearer `. Claude Desktop uses the same connector settings as claude.ai, so there is no config file to edit. - **ChatGPT:** on [chatgpt.com/plugins](https://chatgpt.com/plugins), select **+**, then **Add custom MCP server**. Give the server URL, choose **OAuth** and sign in to QRX when ChatGPT asks. ChatGPT's custom MCP servers don't take API keys. - **Claude Code:** run the sign-in command and sign in to QRX in the browser. If Claude Code doesn't open it, run `/mcp`, select **qrx** and sign in. The API key command saves the server to `.mcp.json` with a reference to `QRX_API_KEY` rather than the key itself, so the file is safe to commit, and Claude Code reads the key from your environment when it starts. - **Cursor:** save as `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for every project, and sign in to QRX when Cursor asks. With a key, Cursor reads the key from your environment. - **VS Code:** save as `.vscode/mcp.json` and sign in to QRX when VS Code asks. With a key, VS Code asks for the key once and stores it securely. - **Windsurf:** add to `~/.codeium/windsurf/mcp_config.json` (Devin Desktop: `~/.config/devin/mcp_config.json`) and sign in to QRX when Windsurf asks. Note `serverUrl`, not `url`. - **Cline:** open **MCP Servers → Configure MCP Servers** and add to `cline_mcp_settings.json`, then sign in to QRX when Cline asks. With a key, put it in place of `qrx_live_…`. `autoApprove` lists the tools that only read. - **Codex CLI:** run `codex mcp add qrx --url https://qrx.codes/mcp` and sign in to QRX in the browser; `codex mcp login qrx` signs in again later. With a key, add `bearer_token_env_var = "QRX_API_KEY"` to the `[mcp_servers.qrx]` entry in `~/.codex/config.toml`. - **Gemini CLI:** run `gemini mcp add --transport http -s user qrx https://qrx.codes/mcp` and sign in to QRX when it asks. If it doesn't, run `/mcp auth qrx`. With a key, add `--header "Authorization: Bearer $QRX_API_KEY"` to the command. - **Other MCP clients, such as Zed, Goose, LM Studio and Kiro:** add the URL above as a Streamable HTTP server and sign in to QRX when the client asks, or send the header `Authorization: Bearer `. - **Clients that only run local (stdio) servers:** use the **Local (stdio)** config, which runs `npx -y @qrxcodes/mcp` with your key in `QRX_API_KEY` and forwards to this server. ## Docs for coding agents {#docs-for-agents} Every page of these docs is also Markdown, and the whole set is in one file, so a coding agent can read it in one go. They are public: no key or sign-in is needed. | File | What it holds | | --- | --- | | `https://qrx.codes/llms.txt` | An index of the docs, with a link to each page’s Markdown | | `https://qrx.codes/llms-full.txt` | Every page in one Markdown file | | `https://qrx.codes/developers.md` and `…/developers/docs/.md` | One page | | `https://qrx.codes/v1/openapi.json` | The OpenAPI 3.1 description | ```bash Shell curl -s https://qrx.codes/llms-full.txt ``` ## Set up a coding agent {#setup-prompt} Paste this into Claude Code, Cursor or another coding agent to have it build a QRX integration in your project: ```text Prompt Add QR code art to this project with the QRX API. 1. Read the QRX docs first: run `curl -s https://qrx.codes/llms-full.txt`. 2. Read the API key from QRX_API_KEY on the server. Never put it in client-side code, a URL, a log or the repository. 3. Create codes with POST /v1/codes?wait=45 and an Idempotency-Key; if a code is still processing, wait for it with GET /v1/codes/{id}?wait=45 until its status is succeeded or failed. 4. Handle the problem types in the docs: back off on 429 using Retry-After, and show the daily-limit problem to the user plainly. 5. Write tests that mock the QRX API rather than calling it. ``` To keep the agent on track in later sessions, add this to your project's `AGENTS.md` or `CLAUDE.md`: ```markdown AGENTS.md ## QRX API - Docs for agents: `curl -s https://qrx.codes/llms-full.txt`. - OpenAPI 3.1: `https://qrx.codes/v1/openapi.json`. - The key is in QRX_API_KEY. Never commit it or send it to a browser. - Codes are asynchronous: create with ?wait=45, then GET /v1/codes/{id}?wait=45 until succeeded or failed. - Send an Idempotency-Key with every create; retry a 429 after Retry-After seconds. ```