# 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=<a recent time>`. 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}

<!-- qrx:your-plan -->

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.
