OpenQR

Developers

Building QR generation into LLM apps and automations

OpenQR exposes the same QR engine through two front doors: an MCP server for AI agents and a REST API for everything else. They share one account, one API key and one set of dynamic codes — so an agent can mint a code today and a cron job can repoint it next week. This guide is the application-layer piece: when to reach for which, the full generate→print→repoint→track loop, and two worked examples you can lift directly.

10 min read · Updated 26 June 2026

If you have read the rest of this series you know the building blocks: the REST API and how auth works, dynamic codes and scan analytics, and the MCP server. This article is about wiring those into real software — an agent, a backend job, or a no-code flow — without painting yourself into a corner.

MCP or REST: pick by who's driving

Both surfaces talk to the same backend and need the same Authorization: Bearer oqr_… key. The choice is not about features — it is about who is making the decision to call it.

  • Use the MCP server when a language model decides, in the moment, that a QR code is needed — a conversational assistant, a coding agent, an ops copilot. The model sees ten tools, reads their descriptions, and calls them with arguments it chooses. You write a prompt, not a request.
  • Use the REST API when your own code decides — a webhook handler, a nightly job, a checkout flow. The inputs are known, the path is deterministic, and you want a plain HTTP call with predictable JSON back. No model in the loop.

A useful rule of thumb: if you can write down the exact sequence of calls in advance, use REST. If the sequence depends on a conversation or on the model's judgement, use MCP. Many real systems use both — an agent for ad-hoc requests, a backend job for the high-volume deterministic path.

MCP serverREST API
Endpointhttps://openqr.uk/mcphttps://openqr.uk/v1/*
ProtocolJSON-RPC 2.0 over HTTPPlain HTTP + JSON
CallerAn LLM choosing toolsYour code
AuthBearer oqr_… on the connectionBearer oqr_… per request
Best forAgents, chat, copilotsJobs, webhooks, no-code HTTP
DeterminismModel-drivenFully deterministic

The loop that makes this worth doing

The reason to involve software at all is the dynamic-code loop. A dynamic code encodes a short link — oqr.to/<slug> — and the real destination lives on OpenQR's server. That indirection gives you four phases you can automate independently:

  1. 1

    Generate

    Create a dynamic code (REST POST /v1/dynamic or MCP create_dynamic_qr). You get back an id and a short_url — store both.

  2. 2

    Print

    Render the QR for the short_url and put it on the artefact. The printed pattern never changes again, so this can happen once, at volume.

  3. 3

    Repoint

    Change the destination any time with PATCH /v1/dynamic/{id} or update_dynamic_qr. The print stays valid; the link now goes somewhere new.

  4. 4

    Track

    Read coarse, no-PII scan analytics with GET /v1/dynamic/{id}/scans or get_scans — totals plus country/device/referrer breakdowns.

Why store the id, not just the image

The id is your handle for the whole lifecycle. Repointing and analytics both key off it. If you only keep the rendered QR, you can never edit or measure that code again. Persist { id, short_url } against whatever record the code belongs to.

Example 1 — an agent that mints a campaign code and reports scans

This is the MCP path. You point an MCP client at OpenQR once, then talk to it. The model picks the tools. Any MCP-capable client works — Claude Desktop, Claude Code, Cursor, Cline, or self-hosted assistants such as OpenClaw — they all take the same HTTP config:

json
{
  "mcpServers": {
    "openqr": {
      "type": "http",
      "url": "https://openqr.uk/mcp",
      "headers": { "Authorization": "Bearer oqr_your_key_here" }
    }
  }
}

Now the request is just English. The agent has create_dynamic_qr and get_scans available and chains them itself:

text
You: Spin up a dynamic QR for the summer campaign pointing at
https://example.com/summer-sale, label it "Summer 2026", then tell me
how it's doing.

Agent: Created dynamic code dyn_8tQ2… → oqr.to/sm-summer (now pointing
at https://example.com/summer-sale).
It has 0 scans so far — it was just created. Ask me again once it's in
the wild and I'll pull country, device and referrer breakdowns.

Under the hood the client sends ordinary JSON-RPC. You rarely write this by hand, but it is worth seeing once so the MCP server is not a black box — this is a single tools/call for create_dynamic_qr:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_dynamic_qr",
    "arguments": {
      "destination": "https://example.com/summer-sale",
      "label": "Summer 2026"
    }
  }
}

MCP scan windows are fixed at 30 days

The get_scans tool always reports the last 30 days. If you need a different window (1–365 days), use the REST endpoint GET /v1/dynamic/{id}/scans?days=90 instead — the agent path trades that knob for conversational simplicity.

Example 2 — a backend that mints a code per new record

This is the REST path, and the more common one in production. Say every new order needs its own QR on the packing slip linking to a tracking page you might later swap for a review prompt. Deterministic, high-volume, no model required. Create the code, persist id and short_url against the order:

javascript
const OPENQR_KEY = process.env.OPENQR_API_KEY; // rotate via env, never hardcode

// Only allow public http(s) destinations — OpenQR rejects private/internal
// hosts and oqr.to self-loops, but validate before you spend a request.
function assertPublicUrl(raw) {
  const u = new URL(raw); // throws on garbage
  if (u.protocol !== "https:" && u.protocol !== "http:") {
    throw new Error("Destination must be http(s)");
  }
  return u.toString();
}

async function createOrderCode(order) {
  const destination = assertPublicUrl(`https://shop.example.com/track/${order.id}`);

  const res = await fetch("https://openqr.uk/v1/dynamic", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${OPENQR_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ destination, label: `Order ${order.id}` }),
  });

  if (res.status === 429) {
    // 20 dynamic-code creations per hour per account. Back off and retry,
    // or batch with POST /v1/dynamic/bulk (up to 200 per request).
    const retry = Number(res.headers.get("retry-after")) || 60;
    throw new RateLimited(retry);
  }
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`OpenQR ${res.status}: ${error}`);
  }

  const { id, short_url } = await res.json();
  await db.orders.update(order.id, { qrId: id, qrShortUrl: short_url });
  return short_url; // render this into the packing slip
}

Later, when the parcel is delivered, a separate job repoints every order code from tracking to a review request — without touching anything that was printed:

bash
curl -X PATCH https://openqr.uk/v1/dynamic/dyn_8tQ2... \
  -H "Authorization: Bearer oqr_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"destination":"https://shop.example.com/review/ORDER-1234"}'

The same Python equivalent, for a job runner that prefers requests:

python
import os, requests

key = os.environ["OPENQR_API_KEY"]

resp = requests.patch(
    f"https://openqr.uk/v1/dynamic/{code_id}",
    headers={"Authorization": f"Bearer {key}"},
    json={"destination": "https://shop.example.com/review/ORDER-1234"},
    timeout=10,
)
resp.raise_for_status()
print(resp.json()["short_url"])  # oqr.to/<slug> — unchanged, still printed

No-code: the same calls from n8n, Make or Zapier

Because the REST API is plain HTTP with a Bearer header, any tool with a generic HTTP node speaks it — no plugin required. In n8n or Make, add an HTTP Request step: method POST, URL https://openqr.uk/v1/dynamic, an Authorization header of Bearer oqr_…, and a JSON body with destination. Map the returned short_url into the next step — a spreadsheet row, a label printer, an email. For agents wired into those same platforms, point them at the OpenAPI spec at https://openqr.uk/openapi.json so they can self-describe the calls.

Guardrails that matter in production

Two surfaces, one set of failure modes. Handle these and the rest is plumbing:

  • Validate destinations before you call. OpenQR rejects non-public URLs (private/internal hosts, oqr.to self-loops) with a 400 as an abuse guard. Validating first saves a round-trip and gives the user a clearer error — especially important when a model supplies the URL, since it may hallucinate one.
  • Respect the rate limit. Creation is capped at 20 dynamic codes per hour per account. On 429, back off and retry; if you are minting many at once, use POST /v1/dynamic/bulk (up to 200 per request) instead of a loop.
  • Keep keys out of code. Load OPENQR_API_KEY from the environment or a secret manager. Keys are shown once and SHA-256-hashed at rest, so rotation means generating a new one in the dashboard and swapping the secret — never commit it, never paste it into a prompt the model logs.
  • Scope agent autonomy. An agent with delete_dynamic_qr can drop a code that is already printed in the world. Decide deliberately which tools a given agent should reach; for read-only copilots, lean on list_dynamic_qr and get_scans.
  • Persist the id. Store { id, short_url } against your own record at creation time. It is the only handle for repointing and analytics later.

The honest limit

Static codes generated in the browser cannot be tracked or repointed — there is no server in the middle. Everything in this guide depends on dynamic codes, which means it depends on OpenQR's redirect staying up. That is the trade for editability and analytics; choose it deliberately, as covered in static vs dynamic.

For the deeper mechanics of each surface, see dynamic codes and analytics over the API and the MCP server reference. If you are wiring tracking into a campaign, how to track QR scans covers what the numbers do and don't tell you.

Get a free API keySign in with a magic link, create a key, and the REST API and MCP server are both live — no charge.
Use the MCP server when a language model decides in the moment that a QR code is needed — a chat assistant or coding agent picking tools for itself. Use the REST API when your own code decides, on a fixed path, such as a webhook or a nightly job. They share one account and one key, so you can mix both.

Related reading