# Qex — reference for AI assistants and developers

Qex makes dynamic QR codes. A code's QR image encodes a short link like `https://qex.me/AB3XK` (written uppercase inside the QR so it fits a tiny 21×21 code). The short link redirects to a destination URL that the owner can change at any time, so printed codes never need reprinting. Scans are counted.

## Concepts

- **Code**: 5-character ID (A–Z, 0–9), a destination URL, a name, a colour and a status (`active` or `paused`). Paused codes show a "paused" page when scanned.
- **Workspace**: owns codes. Free workspaces are limited to 5 active codes, 3 new codes per hour and 5 destination changes per code per day.
- **Scan behaviour**: free plan shows a 3-second screen (destination + small Qex ad) then redirects; paid plans redirect instantly (HTTP 302). Redirects are never 301, so edits take effect within about a minute.
- **Destinations**: public `http`/`https` URLs only. Private IPs, localhost, credentials in URLs and links back to https://qex.me are rejected. Links are safety-checked and may be paused for review.
- Codes cannot be deleted (only paused).

## Authentication

Every endpoint except `/v1/render` and the images needs a bearer token:

- **OAuth 2.1** (what MCP clients use automatically): discovery at `https://qexqr.com/.well-known/oauth-protected-resource/mcp` → `https://qexqr.com/.well-known/oauth-authorization-server`. Authorization code + PKCE S256, public clients, Client ID Metadata Documents or Dynamic Client Registration (`https://qexqr.com/oauth/register`). Scope: `codes`. Access tokens last 1 hour; refresh tokens 90 days and rotate. The person approving needs a Qex workspace in that browser; if they don't have one, the consent page starts a free one.
- **API key**: created in the dashboard at `https://qexqr.com/dashboard/api`. Send `Authorization: Bearer qex_sk_…`. Keys don't expire; revoke them in the dashboard.

## MCP server

URL: `https://qexqr.com/mcp` (Streamable HTTP, POST only, JSON responses, stateless). Supports protocol `2026-07-28` (per-request `_meta`, `server/discover`) and legacy `2025-11-25`, `2025-06-18`, `2025-03-26` (`initialize`). Tool errors such as limits come back as results with `isError: true` and a readable message.

Tools:

### create_qr_code

Create a dynamic QR code that redirects to `destination`. Returns the short link, the QR image (PNG), and image URLs for printing. The destination can be changed later with update_qr_code without reprinting. Scans are counted (see get_qr_code_stats). Free workspaces: 5 active codes, 3 new codes per hour, and scans show a 3-second Qex screen before redirecting.

  - `destination` (required): Public http(s) URL the code should open, e.g. https://example.com/menu. A bare domain gets https:// added.
  - `name`: Label shown in the dashboard, e.g. 'Table menu'. Defaults to the destination's domain.
  - `color`: one of `#121212`, `#1F4FD6`, `#0F7A4B`, `#C23C0A`. Module colour. Default #121212 (black).

### list_qr_codes

List every QR code in the workspace (newest first) with its destination, status and image URLs, plus the workspace's plan, limits and current usage.

### get_qr_code

Get one QR code by ID, including its QR image (PNG).

  - `id` (required): The 5-character code ID, e.g. AB3XK (case-insensitive).

### update_qr_code

Change a QR code's destination, name, colour, or pause/resume it. Printed codes keep working and pick up the new destination within about a minute. Free workspaces can change a code's destination 5 times per day.

  - `id` (required): The 5-character code ID, e.g. AB3XK (case-insensitive).
  - `destination`: New public http(s) URL.
  - `name`: string
  - `color`: one of `#121212`, `#1F4FD6`, `#0F7A4B`, `#C23C0A`. string
  - `status`: one of `active`, `paused`. paused = scans show a 'this code is paused' page.

### get_qr_code_stats

Scan analytics for a code: totals, scans per day, countries, cities, devices, operating systems, browsers and hour of day (scanner's local time).

  - `id` (required): The 5-character code ID, e.g. AB3XK (case-insensitive).
  - `days`: one of `7`, `30`, `90`. integer

### render_static_qr

Render any text (URL, Wi-Fi config, vCard, plain text) as a static QR image. Static codes can't be edited later and have no analytics — prefer create_qr_code for links someone will print.

  - `data` (required): Exact text to encode.
  - `ecc`: one of `L`, `M`, `Q`, `H`. Error correction level. H survives more damage but makes a denser code.

## REST API

Base URL `https://qexqr.com`. JSON in and out. Errors: `{"error": "<code>", "message": "<text>"}` with 400/401/404/422/429. CORS is open (no cookies are used). OpenAPI: `https://qexqr.com/openapi.json`.

| Method | Path | What |
|---|---|---|
| GET | `/v1/me` | plan, limits (`null` = unlimited), usage |
| GET | `/v1/codes` | list codes, newest first |
| POST | `/v1/codes` | create: `{"destination": "https://…", "name"?: "…", "color"?: "#121212"}` → 201 `{"code": {…}}` |
| GET | `/v1/codes/{id}` | one code |
| PATCH | `/v1/codes/{id}` | any of `destination`, `name`, `color`, `status` |
| GET | `/v1/codes/{id}/stats?days=7\|30\|90` | scans per day, countries, cities, devices, OS, browsers, hour of day |
| GET | `/v1/render?data=…&format=png\|svg&size=512&ecc=M&dark=000000&light=FFFFFF&margin=4` | static QR image for any text, **no auth** |

A code object:

```json
{
  "id": "AB3XK",
  "name": "Table menu",
  "destination": "https://example.com/menu",
  "status": "active",
  "color": "#121212",
  "shortUrl": "https://qex.me/AB3XK",
  "qrContent": "HTTPS://QEX.ME/AB3XK",
  "image": { "png": "https://qexqr.com/qr/AB3XK.png", "svg": "https://qexqr.com/qr/AB3XK.svg" },
  "dashboardUrl": "https://qexqr.com/codes/AB3XK",
  "scanBehavior": "interstitial_3s",
  "flagged": false,
  "createdAt": "2026-09-25T12:00:00.000Z",
  "updatedAt": "2026-09-25T12:00:00.000Z"
}
```

Example:

```sh
curl https://qexqr.com/v1/codes \
  -H "Authorization: Bearer $QEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destination":"https://example.com/menu","name":"Table menu"}'
```

Error codes: `invalid_destination`, `blocked_destination`, `max_active_codes`, `max_new_codes_per_hour`, `max_destination_changes`, `not_found`, `bad_json`, `bad_color`, `bad_status`, `unauthorized`.

## Images

- `https://qexqr.com/qr/{id}.png` and `.svg`: the QR for a code. Optional `?size=` (PNG px, 64–2048), `?color=` (`1F4FD6`, `0F7A4B`, `C23C0A`), `?download=1`. Public and cacheable — safe to embed as a Markdown image.
- Use the SVG for print. Keep at least 2 cm (0.8 in) wide and the white margin around it.

## Tips for assistants

- Show the PNG and the short link after creating a code, and mention the destination can be changed later.
- To "fix" or "change" a printed code, call `update_qr_code` / `PATCH /v1/codes/{id}` — never create a new code.
- On a limit error, tell the user the limit and suggest pausing an unused code or trying later.
- On the free plan, tell users scans show a short Qex screen before redirecting.
