QRFLOW.codes

official

Create QR codes, re-point printed dynamic codes, name links on your own domain, and read scan analytics.

What can you do with QRFLOW Codes MCP?

  • Create dynamic QR codes — Ask to generate a printable short_url for any URL, with the destination changeable anytime after printing.
  • Repoint printed codes — Request updating a code's destination_data to a new URL; existing printed codes keep working without reprinting.
  • Pull scan analytics — Ask for Scans breakdowns by day, country, or device to see how a printed code is performing.
  • Pause or expire codes — Instruct to pause a code or set an expiration date when a campaign or offer ends.
  • Name links on your domain — Request setting a readable slug on your own domain so codes print as go.yourbrand.com/menu instead of a random path.

Hosted MCP Server

npx add-mcp 'https://qrflow.codes/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

QRFLOW.codes for developers

Start here

QRFLOW.codes makes QR codes you can change after printing, on your own domain, with scan analytics, and lets your software and your AI assistant do all of it through one API.

A QR code is a picture of a string. Most generators put your web address in the picture; if the address changes, the print is dead. QRFLOW puts a short link in the picture (go.yourbrand.com/menu or qrflow.codes/q/x7k2p9a) and redirects it. You change where it points whenever you like, and every scan is counted. That is a dynamic code, and it is the thing worth an API.

The API does exactly what the dashboard does: create codes of any kind, point them somewhere new, name their links, choose which of your domains they print with, pull scan numbers, make thousands at once, and get told when something happens (webhooks). The same tools are exposed as an MCP server, so Claude, ChatGPT, Cursor and Claude Code can drive your account in plain words.

Access: the REST API takes a Business API key ($29 a month, made on the Account page) or an OAuth token. The MCP server works on every plan through OAuth sign-in; what the assistant may create follows the plan like everywhere else.

Deciding whether to use it at all? The QR code API page is the short version: what it does, what a client-side library does better, and what it costs.

If you are an AI assistant helping someone build with QRFLOW

Welcome. This page is written for you as much as for the person you are helping. Here is what good help looks like:

  1. Confirm the account: API keys need the Business plan. If they do not have one, point them to https://qrflow.codes/pricing and Account › API keys; do not invent a key or a free tier that does not exist.
  2. Keep the key on the server. Read it from an environment variable (QRFLOW_KEY) in a route handler, server action, edge function or backend. The API refuses browser origins on purpose (CORS), so front-end-only code cannot call it.
  3. Create dynamic codes with type "url" and destination_data.url. Save the returned id and short_url in their database. Print or display short_url; the destination can be changed later with PATCH /codes/:id and the print stays valid.
  4. To show or save the image: every Code object has png_download_url and svg_download_url, signed links that work for 24 hours with no header (fine for a one-off save or a preview). For something permanent in their app, proxy GET /codes/:id/image.svg through their server with the key, or encode short_url with any QR library on the client; the tracking still works because the link goes through QRFLOW.
  5. Send only the fields that change on PATCH. Never change slug or domain_id after something is printed; both change the printed link.
  6. Prefer paused: true over DELETE when a print exists. Deleting kills the link.
  7. For events, use webhooks (Account › Webhooks or POST /webhooks) and verify the X-QRFLOW-Signature header with the raw request body. Do not poll GET /codes in a loop.
  8. If they want to use QRFLOW from their chat instead of from code, connect the MCP server at https://qrflow.codes/mcp; no key is needed for that.
  9. When something fails, read error and message in the JSON body. The troubleshooting section below maps every error code to a fix.
  10. The complete reference in Markdown is at https://qrflow.codes/llms-full.txt and the OpenAPI 3.1 document at https://qrflow.codes/api/v1/openapi.json. Both are generated from the same source as this page.

Humans: this block is how we make sure the assistant you are pairing with gives you the safe version of every answer. It also happens to be a good summary.

Which plan you need

The API key is the only thing that is Business-only. Everything an assistant does through the MCP server, and everything a third-party app does through OAuth, works on any plan and simply follows that plan's features. Full pricing and fair use: /pricing.

FreePremium $4Business $29
API keys (REST from your code)NoNoYes, up to 10 keys
MCP server (Claude, ChatGPT, Cursor, Claude Code)Yes, sign inYesYes, sign in or key
OAuth for your own app (users connect their QRFLOW)YesYesYes
Dynamic codes (change destination after printing)No, static onlyYesYes
Your own link domainNo1 domain5 domains, pick per code
Link names (go.brand.com/menu)NoYesYes
Scan analyticsNoYesYes
WebhooksNoNoYes, up to 10
Bulk createNo500/month, 500 per request10,000/month, 2,000 per request
Saved codes (fair use)A handful1,00025,000
Team seats115
Price$0$4/month$29/month

Twelve words that do all the work

Concepts

Read these once and every endpoint below will make sense.

Static code

The content is inside the picture. Wi-Fi, contact card (vCard) and plain text codes are always static, and on the Free plan every code is. A static code needs no server and never expires, and it cannot be changed or counted.

Dynamic code

The picture holds a short link that QRFLOW redirects. url, phone, email, sms and location codes are dynamic on Premium and Business. You can re-point, pause, expire, rename and count them without touching the print.

short_url

The exact string encoded in a dynamic code, and the thing to print. It is https://qrflow.codes/q/<short_code> until you connect a domain, then https://<your domain>/<slug or short_code>. Every Code object carries it.

short_code

Seven random characters, unique per code, assigned at creation and never changed. The fallback path when a code has no link name.

slug (link name)

A readable path on your own domain: go.example.com/menu. 3 to 40 lowercase letters, digits and dashes, unique within your account, only with a connected domain. Set it before you print: changing it changes the printed link.

Link domain

A hostname you own (go.example.com) pointed at QRFLOW by a CNAME and verified on the Account page. Premium gets one; Business gets five and can choose per code with domain_id. The oldest live domain is the default.

Kind, type and subtype

type is the encoding: url, text, wifi, vcard, email, phone, sms, location. A kind is a friendlier name for what people want (instagram, googlereview, whatsapp, pdf, menu, appstore,...). Most kinds are url codes with destination_data.subtype set. GET /catalog lists every kind with its fields; kind on a Code tells you which one it is.

destination_data

The fields for the type, as strings: { url } for a website, { ssid, password, encryption } for Wi-Fi, { placeId } for a Google review, { handle } for Instagram. On a dynamic code you can replace it any time.

Scans

Each redirect records device type, country, city, referrer, browser, OS and language from the request itself, plus a one-way daily hash for counting unique visitors. No cookies are set and the IP address is not stored. scans on a Code is the lifetime total; GET /codes/:id/scans breaks it down.

Source

Every code remembers what made it: dashboard, api:<key name>, mcp, canva or bulk. It shows in the dashboard and in webhook payloads, so you can tell your integration's codes from hand-made ones.

Workspace

A Business owner can invite up to four teammates. Keys and webhooks belong to the owner's account; codes made by anyone in the workspace are visible to the whole team.

Five minutes

Quick start

Get a key

  1. On the Business plan, open Account › API keys.
  2. Name it after the thing it is for ("Shop backend", "Reporting") and pick its scopes. Scopes cannot be changed later; make a new key if you need more.
  3. Copy it once. It looks like qrf_live_…. Put it in an environment variable named QRFLOW_KEY.
  4. Send it as Authorization: Bearer $QRFLOW_KEY on every request. That is the whole authentication story.

Keys are for servers. Never put one in a web page, a mobile app or a shared spreadsheet; revoke and re-issue if one leaks.

The same five steps in curl, TypeScript and Python. Each one creates a dynamic code, downloads its image, changes where it points, and reads its scans.

export QRFLOW_KEY=qrf_live_...   # from Account › API keys

# 1. Who am I, what can this key do?
curl https://qrflow.codes/api/v1/me -H "Authorization: Bearer $QRFLOW_KEY"

# 2. Make a dynamic code. Print what comes back as short_url.
curl -X POST https://qrflow.codes/api/v1/codes \
  -H "Authorization: Bearer $QRFLOW_KEY" -H "Content-Type: application/json" \
  -d '{ "type": "url", "destination_data": { "url": "https://example.com/menu" }, "label": "Table tents" }'

# 3. The print-ready image (SVG, with your colors and frame).
curl "https://qrflow.codes/api/v1/codes/$CODE_ID/image.svg?size=1024" \
  -H "Authorization: Bearer $QRFLOW_KEY" -o menu.svg

# 4. Fall menu. The printed code keeps working.
curl -X PATCH https://qrflow.codes/api/v1/codes/$CODE_ID \
  -H "Authorization: Bearer $QRFLOW_KEY" -H "Content-Type: application/json" \
  -d '{ "destination_data": { "url": "https://example.com/menu-fall" } }'

# 5. How did it do?
curl "https://qrflow.codes/api/v1/codes/$CODE_ID/scans?group=day" -H "Authorization: Bearer $QRFLOW_KEY"

TypeScript (Node 18+, Bun, Deno, Workers)

// npm install qrflow   (zero dependencies; ESM + CommonJS; full types)
import { QRFlow, QRFlowError } from "qrflow";

const qr = new QRFlow(process.env.QRFLOW_KEY!);

const { code } = await qr.createCode({
  type: "url",
  destination_data: { url: "https://example.com/menu" },
  label: "Table tents",
});
console.log(code.id, code.short_url);        // save both; print short_url

await qr.updateCode(code.id, { destination_data: { url: "https://example.com/menu-fall" } });

const stats = await qr.scans(code.id, { group: "day" });
console.log(stats.total, stats.rows);         // [{ key: "2026-09-21", scans: 18 }, ...]

try {
  await qr.updateCode(code.id, { slug: "menu" });
} catch (e) {
  if (e instanceof QRFlowError) console.log(e.status, e.code, e.message); // 400 no_domain: connect a domain first
}
# Download https://qrflow.codes/sdk/qrflow.py next to your code.
import os
from qrflow import QRFlow, QRFlowError

qr = QRFlow(os.environ["QRFLOW_KEY"])

code = qr.create_code(type="url", destination_data={"url": "https://example.com/menu"}, label="Table tents")["code"]
print(code["id"], code["short_url"])          # save both; print short_url

qr.update_code(code["id"], destination_data={"url": "https://example.com/menu-fall"})

stats = qr.scans(code["id"], group="day")
print(stats["total"], stats["rows"])

try:
    qr.update_code(code["id"], slug="menu")
except QRFlowError as e:
    print(e.status, e.code, e)                # 400 no_domain: connect a domain first

Authentication

API keys (Business)

Up to 10 per account, 600 requests a minute each, stored hashed, shown once. A key carries the scopes it was made with:

ScopeAllows
profileGET /me: plan, features, limits. Every key has it.
codes:readList and read codes, download images.
codes:writeCreate, change, make dynamic, delete, bulk create.
analytics:readGET /codes/:id/scans.
domains:readGET /domains (needed to use domain_id sensibly).
webhooks:manageList, create, test and delete webhooks.

OAuth 2.0 (any plan, for apps and assistants)

When the codes should belong to your users' accounts rather than yours, or when a chat assistant is the client, use OAuth. Clients register themselves; PKCE S256 is required for public clients; tokens can be bound to the MCP server with resource=. The OAuth recipe walks through it.

EndpointURLNotes
Authorizationhttps://qrflow.codes/oauth/authorizeSend the user here; they sign in and press Allow.
Tokenhttps://qrflow.codes/api/oauth/tokenauthorization_code and refresh_token grants. Access tokens live 1 hour, refresh tokens 90 days.
Revokehttps://qrflow.codes/api/oauth/revokeRFC 7009. Users can also disconnect on Account › Connected apps.
Register a clienthttps://qrflow.codes/api/oauth/registerRFC 7591 dynamic registration, no account needed. Public clients get a dyn_ client_id and must use PKCE S256.
Discoveryhttps://qrflow.codes/.well-known/oauth-authorization-serverRFC 8414. The MCP resource document is at /.well-known/oauth-protected-resource.

Access tokens last 1 hour, refresh tokens 90 days. Users see connected apps on Account › Connected apps and can disconnect any time. A token minted for https://qrflow.codes/mcp is refused on /api/v1, and the other way around.

The things people actually ask for

Build recipes

Each recipe is complete and copied straight from working code. Pick the one that matches your stack; the shape is always the same: a server-side call with the key, save id and short_url, show the image.

Next.js: a route that makes a code and a route that shows it

When: You have a Next.js app (App Router) and want a button that creates a QR code and a page that displays it.

  1. Put your key in.env.local as QRFLOW_KEY. Never prefix it with NEXT_PUBLIC_.
  2. Add a POST route handler that creates the code and returns id and short_url.
  3. Add a GET route that proxies the image so the browser never sees the key.
  4. Store id and short_url on your own record (order, table, product, event).
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { url, label } = await req.json();
  const r = await fetch("https://qrflow.codes/api/v1/codes", {
    method: "POST",
    headers: { Authorization: \`Bearer ${process.env.QRFLOW_KEY}\`, "Content-Type": "application/json" },
    body: JSON.stringify({ type: "url", destination_data: { url }, label }),
  });
  const data = await r.json();
  if (!r.ok) return NextResponse.json(data, { status: r.status }); // { error, message }
  return NextResponse.json({ id: data.code.id, short_url: data.code.short_url });
}
// Proxies the SVG so the key stays on the server. Check that the signed-in
// user owns this id before you serve it, or anyone with an id can fetch it.
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const r = await fetch(\`https://qrflow.codes/api/v1/codes/${id}/image.svg?size=1024\`, {
    headers: { Authorization: \`Bearer ${process.env.QRFLOW_KEY}\` },
  });
  return new Response(r.body, {
    status: r.status,
    headers: { "Content-Type": "image/svg+xml", "Cache-Control": "private, max-age=3600" },
  });
}
<img src={\`/api/qr/${code.id}/image\`} alt={\`QR code for ${code.label}\`} width={256} height={256} />
<a href={code.short_url}>{code.short_url}</a>
  • Server Actions work the same way: call fetch with the key inside the action.
  • For Pages Router, the same code goes in pages/api/qr.ts with req/res.

Show the QR without proxying: render short_url yourself

When: You want the image in the browser right away and do not need QRFLOW's frames or logo on it.

Any QR library works, because the code IS the short link

import QRCode from "qrcode"; // npm i qrcode

// short_url came back from POST /codes. Encode it as-is.
const dataUrl = await QRCode.toDataURL(code.short_url, { width: 512, margin: 2 });
// <img src={dataUrl} />  scans go through QRFLOW, so analytics and re-pointing still work.
  • This is the fastest path for a preview. For print, download /codes/:id/image.svg: it carries the saved colors, frame, captions and logo, and it is a vector.
  • If you later change slug or domain_id, short_url changes; re-render.

Express or any Node server

When: A plain Node backend.

import express from "express";
const app = express();
app.use(express.json());
const H = { Authorization: \`Bearer ${process.env.QRFLOW_KEY}\`, "Content-Type": "application/json" };

app.post("/qr", async (req, res) => {
  const r = await fetch("https://qrflow.codes/api/v1/codes", {
    method: "POST", headers: H,
    body: JSON.stringify({ type: "url", destination_data: { url: req.body.url }, label: req.body.label }),
  });
  res.status(r.status).json(await r.json());
});

app.get("/qr/:id.svg", async (req, res) => {
  const r = await fetch(\`https://qrflow.codes/api/v1/codes/${req.params.id}/image.svg\`, { headers: H });
  res.status(r.status).type("image/svg+xml").send(await r.text());
});

app.listen(3000);

Cloudflare Workers, Vercel Edge, Deno Deploy, Supabase Edge Functions

When: A fetch-only runtime with no Node built-ins.

export default {
  async fetch(req: Request, env: { QRFLOW_KEY: string }) {
    const { url, label } = await req.json();
    const r = await fetch("https://qrflow.codes/api/v1/codes", {
      method: "POST",
      headers: { Authorization: \`Bearer ${env.QRFLOW_KEY}\`, "Content-Type": "application/json" },
      body: JSON.stringify({ type: "url", destination_data: { url }, label }),
    });
    return new Response(r.body, { status: r.status, headers: { "Content-Type": "application/json" } });
  },
};
Deno.serve(async (req) => {
  const { url, label } = await req.json();
  const r = await fetch("https://qrflow.codes/api/v1/codes", {
    method: "POST",
    headers: { Authorization: \`Bearer ${Deno.env.get("QRFLOW_KEY")}\`, "Content-Type": "application/json" },
    body: JSON.stringify({ type: "url", destination_data: { url }, label }),
  });
  return new Response(await r.text(), { status: r.status, headers: { "Content-Type": "application/json" } });
});
// supabase secrets set QRFLOW_KEY=qrf_live_...
  • The npm package `qrflow` uses only fetch and WebCrypto, so it runs in all of these unchanged.

Python: FastAPI, Flask, Django, a script

When: Your backend is Python.

import os
from fastapi import FastAPI, HTTPException, Response
from qrflow import QRFlow, QRFlowError   # https://qrflow.codes/sdk/qrflow.py

app = FastAPI()
qr = QRFlow(os.environ["QRFLOW_KEY"])

@app.post("/qr")
def make_qr(url: str, label: str | None = None):
    try:
        code = qr.create_code(type="url", destination_data={"url": url}, label=label)["code"]
    except QRFlowError as e:
        raise HTTPException(e.status, {"error": e.code, "message": str(e)})
    return {"id": code["id"], "short_url": code["short_url"]}

@app.get("/qr/{code_id}.svg")
def qr_image(code_id: str):
    import urllib.request
    req = urllib.request.Request(qr.image_url(code_id), headers={"Authorization": f"Bearer {os.environ['QRFLOW_KEY']}"})
    with urllib.request.urlopen(req) as r:
        return Response(r.read(), media_type="image/svg+xml")

One QR code per order, table, product, ticket or event

When: Every row in one of your tables needs its own code, made automatically.

  1. Add two columns to your table: qrflow_code_id (uuid) and qr_short_url (text).
  2. When a row is created, POST /codes with the row's public URL and a label that names the row ("Order 10432", "Table 7"). Save id and short_url.
  3. When the row's page moves (new domain, new path), PATCH destination_data. The printed codes keep working.
  4. When the row is retired, PATCH { paused: true } if anything was printed; DELETE only if nothing was.
  5. Need thousands at once (a menu per table for 300 restaurants)? Use POST /codes/bulk in chunks and map the returned codes back to your rows by label or by order.
const { code } = await qr.createCode({
  type: "url",
  destination_data: { url: \`https://example.com/orders/${order.id}\` },
  label: \`Order ${order.number}\`,
});
await db.orders.update(order.id, { qrflow_code_id: code.id, qr_short_url: code.short_url });
  • Fair use is 25,000 saved codes on Business; the API stops at twice that. If you need one code per receipt forever, talk to us first: hello@qrflow.codes.

Change where a printed code goes

When: A campaign ended, a page moved, a PDF was replaced, a season changed.

curl -X PATCH https://qrflow.codes/api/v1/codes/$CODE_ID \
  -H "Authorization: Bearer $QRFLOW_KEY" -H "Content-Type: application/json" \
  -d '{ "destination_data": { "url": "https://example.com/spring" } }'

Pause it, or give it an end date

# Scans show a "paused" page instead of redirecting
curl -X PATCH https://qrflow.codes/api/v1/codes/$CODE_ID -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" -d '{ "paused": true }'

# Stops working after the date; null clears it
curl -X PATCH https://qrflow.codes/api/v1/codes/$CODE_ID -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" -d '{ "expires_at": "2026-12-31T23:59:59Z" }'
  • Only dynamic codes can be re-pointed. A url code created on a Free account, or a Wi-Fi/vCard/text code, answers 400 not_dynamic. For a url/phone/email/sms/location code on a paid plan, POST /codes/:id/dynamic converts it, and you must re-render and re-print because the picture changes.
  • A scanner sees the new destination on the next scan. There is no cache to wait out.

Print codes on your own domain

When: You want go.example.com/menu in the code instead of qrflow.codes/q/x7k2p9a.

  1. On the Account page, under Your own link domain, add go.example.com and create the CNAME it shows you at your DNS provider. Verification usually completes in minutes.
  2. From then on every new dynamic code's short_url uses that domain. Existing codes switch too: their picture encoded qrflow.codes/q/..., which keeps redirecting, so nothing printed breaks.
  3. Give codes readable names with slug: PATCH { "slug": "menu" } makes go.example.com/menu. Do this before printing.
  4. On Business with several domains, GET /domains lists them with ids; pass domain_id on POST or PATCH to choose per code.

Name a link and pick a domain

curl https://qrflow.codes/api/v1/domains -H "Authorization: Bearer $QRFLOW_KEY"
# { "default_base": "https://go.example.com", "domains": [ { "id": "…", "host": "go.example.com", "is_default": true, … }, { "id": "…", "host": "qr.example.fr", … } ] }

curl -X PATCH https://qrflow.codes/api/v1/codes/$CODE_ID -H "Authorization: Bearer $QRFLOW_KEY" -H "Content-Type: application/json" \
  -d '{ "slug": "menu", "domain_id": "<id of qr.example.fr>" }'
# short_url is now https://qr.example.fr/menu

A scan chart in your own admin

When: You want scans per day, per country or per device next to your own numbers.

const stats = await qr.scans(code.id, { from: "2026-09-01", to: "2026-09-30", group: "day" });
// stats.total -> 412
// stats.rows  -> [{ key: "2026-09-01", scans: 18 }, { key: "2026-09-02", scans: 25 }, ...]
// Feed rows straight into Recharts, Chart.js, or a <table>.

const byCountry = await qr.scans(code.id, { group: "country" });   // [{ key: "US", scans: 300 }, { key: "MX", scans: 41 }]
const byDevice  = await qr.scans(code.id, { group: "device" });    // mobile, desktop, tablet
  • One request covers up to 92 days; loop for longer ranges. Dates are UTC.
  • For live numbers without polling, subscribe a webhook to the scan event: you get every scan with its details in batches every few minutes.

Receive webhooks in Next.js and verify them

When: You want to know when a code is scanned or changed, in your own database, in near real time.

  1. Create the webhook on Account › Webhooks or with POST /webhooks. Copy the secret (whsec_...) once into QRFLOW_WEBHOOK_SECRET.
  2. Read the raw body as text before parsing; the signature covers the exact bytes.
  3. Verify, then switch on event. Answer 2xx fast; do slow work after you respond or in a queue.
  4. Press Test on the webhook to receive a signed ping and confirm the wiring.
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(raw: string, header: string, secret: string): boolean {
  const t = /t=(\d+)/.exec(header)?.[1], v1 = /v1=([a-f0-9]+)/.exec(header)?.[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(\`${t}.${raw}\`).digest("hex");
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

export async function POST(req: Request) {
  const raw = await req.text();
  if (!verify(raw, req.headers.get("x-qrflow-signature") ?? "", process.env.QRFLOW_WEBHOOK_SECRET!)) {
    return new Response("bad signature", { status: 401 });
  }
  const evt = JSON.parse(raw) as { id: string; event: string; created_at: string; data: any };
  // evt.id is stable across retries: store it and skip duplicates.

  switch (evt.event) {
    case "scan":          // evt.data.scans[]: code_id, label, slug, scanned_at, device, country, city, referrer, browser, os, language
      break;
    case "code.created":  // evt.data.code, evt.data.source
    case "code.updated":  // evt.data.code, evt.data.changed[]
    case "code.deleted":  // evt.data.code { id, label, short_code, slug }
      break;
    case "ping":          // the Test button
      break;
  }
  return new Response(null, { status: 204 });
}
  • Locally, expose your dev server with a tunnel (cloudflared tunnel --url http://localhost:3000, or ngrok) and use that https URL for the webhook while you build.
  • The npm package does this for you: `import { parseWebhook } from "qrflow"` verifies and parses in one call (WebCrypto, so it also runs in Workers and Deno). The Python client ships verify_webhook.

Receive webhooks in Python

When: Flask, FastAPI or Django receiving the same events.

import os, json
from flask import Flask, request, abort
from qrflow import verify_webhook   # https://qrflow.codes/sdk/qrflow.py

app = Flask(__name__)

@app.post("/qrflow")
def hook():
    raw = request.get_data()  # bytes, before any parsing
    if not verify_webhook(raw, request.headers.get("X-QRFLOW-Signature", ""), os.environ["QRFLOW_WEBHOOK_SECRET"]):
        abort(401)
    evt = json.loads(raw)
    if evt["event"] == "scan":
        for s in evt["data"]["scans"]:
            print(s["code_id"], s["scanned_at"], s["country"], s["device"])
    return "", 204

Thousands of codes from a CSV

When: A code per SKU, per seat, per asset tag, per mailer.

import { parse } from "csv-parse/sync";
import { readFileSync } from "node:fs";

const rows = parse(readFileSync("skus.csv"), { columns: true }) as Array<{ sku: string; url: string }>;
const out: Array<{ sku: string; id: string; short_url: string }> = [];

for (let i = 0; i < rows.length; i += 2000) {                 // Business: 2,000 per request
  const chunk = rows.slice(i, i + 2000);
  const { codes, rejected, remaining_this_month } = await qr.bulkCreate(
    chunk.map((r) => ({ destination: r.url, label: r.sku })),
  );
  codes.forEach((c, j) => out.push({ sku: chunk[j].sku, id: c.id, short_url: c.short_url }));
  if (rejected.length) console.warn(rejected);                  // rows that were not web addresses
  console.log(remaining_this_month, "left this month");
}
  • Bulk makes dynamic url codes only, all with the same colors. Codes come back in the order you sent them, minus rejected rows; match by label when in doubt.
  • Webhooks get one code.created event per bulk request with codes[] instead of one per code.

Let your users connect their own QRFLOW account (OAuth)

When: You are building a product for other people and want codes to land in their QRFLOW accounts, not yours.

  1. Register a client once: POST https://qrflow.codes/api/oauth/register with client_name and redirect_uris. You get a client_id (and a client_secret for confidential clients).
  2. Send the user to /oauth/authorize with response_type=code, client_id, redirect_uri, scope, state, and PKCE (code_challenge, code_challenge_method=S256).
  3. Exchange the code at /api/oauth/token. Store the refresh token; access tokens last an hour.
  4. Call /api/v1 with Authorization: Bearer <access_token>. Everything works exactly as with a key, under the user's plan.
curl -X POST https://qrflow.codes/api/oauth/register -H "Content-Type: application/json" \
  -d '{ "client_name": "Acme Menus", "redirect_uris": ["https://app.example.com/oauth/qrflow"], "token_endpoint_auth_method": "none" }'
# { "client_id": "dyn_…", "redirect_uris": [...], "grant_types": ["authorization_code","refresh_token"], … }
https://qrflow.codes/oauth/authorize?response_type=code&client_id=dyn_…&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fqrflow
  &scope=profile%20codes%3Aread%20codes%3Awrite%20analytics%3Aread&state=…&code_challenge=…&code_challenge_method=S256
  • Scopes are the same six as API keys. Ask for the least you need; the consent screen lists them.
  • If your app is a chat assistant or agent, add resource=https://qrflow.codes/mcp to the authorize request and talk to the MCP server instead; the token will be bound to it.

Zapier, Make, n8n: no code at all

When: You want scans or new codes to land in a sheet, a Slack channel or a CRM.

  1. Create a catch-hook trigger (Zapier: Webhooks by Zapier › Catch Hook; Make: Custom webhook; n8n: Webhook node) and copy its https URL.
  2. On Account › Webhooks, add that URL and pick the events. Press Test; the ping shows up in the tool and gives it the payload shape.
  3. Map data.scans[] (for scan) or data.code (for code.*) to your sheet, message or record.
  4. To create codes from those tools, use their HTTP module against POST /codes with the Authorization header. Keep the key in the tool's credential store.
  • These tools cannot verify the signature. The URL they give you is unguessable, which is the protection you have; do not post it anywhere.

Vibe coding

Prompts to paste

Coding assistants build the right thing when they are told the rules up front. These prompts carry the rules. Paste one, fill in the bracket, and the assistant will read the Markdown reference before it writes a line.

Add QR codes to my app

Claude, ChatGPT, Cursor, Codex, Windsurf, Copilot Chat: paste into the chat

Add QR codes to this project using the QRFLOW.codes API.

Read https://qrflow.codes/llms-full.txt before writing code; it is the complete reference.

Rules:
- The API key is in the environment variable QRFLOW_KEY. It must only be used server-side (route handler, server action, edge function). Never expose it to the browser.
- Create dynamic codes: POST https://qrflow.codes/api/v1/codes with { "type": "url", "destination_data": { "url": ... }, "label": ... }.
- Save the returned code.id and code.short_url on my record. short_url is what gets printed or displayed.
- Show the image by proxying GET /codes/:id/image.svg through my server, or by encoding short_url with a QR library on the client.
- Handle errors from the JSON body: { "error", "message" }. Map 402 to "upgrade needed", 429 to a retry with the Retry-After header.

What I want: [describe the feature, e.g. "every event in my events table gets a QR code that opens its public page; show it on the event admin page with a download button"].

Teach your repo about QRFLOW

Drop into CLAUDE.md, AGENTS.md,.cursorrules or.github/copilot-instructions.md

## QR codes (QRFLOW.codes)
- Docs: https://qrflow.codes/llms-full.txt (Markdown), https://qrflow.codes/api/v1/openapi.json (OpenAPI 3.1).
- Base URL https://qrflow.codes/api/v1, header Authorization: Bearer $QRFLOW_KEY. Server-side only.
- Codes are created with POST /codes { type: "url", destination_data: { url }, label }. Store code.id and code.short_url.
- Change the destination with PATCH /codes/:id { destination_data: { url } }. Never change slug/domain_id after printing.
- Image: GET /codes/:id/image.svg (needs the key). Prefer paused: true over DELETE when a print exists.
- Webhooks arrive as POST with X-QRFLOW-Signature (t=,v1=HMAC-SHA256 of "t.rawBody"); verify with the raw body.

In Lovable, Bolt, v0, Replit and other app builders

Front-end-first builders that give you a backend (Supabase, serverless functions)

Integrate QRFLOW.codes QR codes. The API refuses browser calls, so create a backend function (Supabase Edge Function / serverless function) that holds the secret QRFLOW_KEY and calls POST https://qrflow.codes/api/v1/codes with { "type": "url", "destination_data": { "url": "<the page URL>" }, "label": "<name>" }. Return code.id and code.short_url to the UI and save them in the database. Render the QR in the UI by encoding short_url with a QR library; add a "Download for print" button that fetches /codes/:id/image.svg through the same backend function. Reference: https://qrflow.codes/llms-full.txt

A custom GPT that manages my codes

ChatGPT › Create a GPT › Actions

Import from URL: https://qrflow.codes/api/v1/openapi.json
Authentication: API Key › Bearer › paste a Business key with the scopes you want the GPT to have.
Then the GPT can list, create, re-point and report on your codes. For a no-setup version, add the MCP connector instead (Developer mode › Plugins › https://qrflow.codes/mcp).

Things to say once the connector is on

Claude, ChatGPT, Claude Code with the QRFLOW MCP server connected

"Make a QR code for https://example.com/fall-menu, call it Fall menu, frame caption 'Scan for menu'."
"Which of my codes got the most scans this month? Show a breakdown by country for the top one."
"Point the 'Lobby poster' code at https://example.com/events/october."
"Name the 'Business card' code's link 'hi' on my domain."
"Pause every code with 'Summer' in the label."
"Make 40 codes, one per table, going to https://example.com/order?table=1 through 40."
"Show me the PNG of the 'Front door' code."

MCP server

Use it from Claude, ChatGPT, Cursor and Claude Code

QRFLOW is an MCP server at https://qrflow.codes/mcp. Connect it once, sign in, and then say things like "make a QR code for our fall menu page, named menu on my domain", "point the lobby poster's code at the new page" or "how many scans did the flyer get last week, by country?". The assistant gets the same tools this API offers, under the same rules, and the codes land in your dashboard with mcp as their source.

Not a developer? The plain-words version, with the exact clicks for each assistant, is at Make QR codes with your AI assistant.

Connect

Customize › Connectors › Add custom connector › paste the address, or press Connect on QRFLOW's directory listing. Claude opens a QRFLOW sign-in; press Allow. Works on every plan.

https://qrflow.codes/mcp

Settings › Security and login › Developer mode on, then Settings › Plugins › + › paste the address; sign in when asked. Plus, Pro, Team, Enterprise and Edu.

https://qrflow.codes/mcp

One command, then /mcp to sign in. Add a Business key as a header to skip the sign-in.

claude mcp add --transport http qrflow https://qrflow.codes/mcp
# or, with a key:
claude mcp add --transport http qrflow https://qrflow.codes/mcp --header "Authorization: Bearer $QRFLOW_KEY"

Cursor, Windsurf, VS Code, any MCP client

Add an HTTP server at the URL. OAuth sign-in happens in the browser; or pass an Authorization header with a key.

{
  "mcpServers": {
    "qrflow": { "type": "http", "url": "https://qrflow.codes/mcp" }
  }
}

Your own agent (Anthropic or OpenAI SDK)

Point the MCP connector or tool at the URL with a Business key as bearer; no browser flow needed.

// Anthropic Messages API, MCP connector
mcp_servers: [{ type: "url", url: "https://qrflow.codes/mcp", name: "qrflow", authorization_token: process.env.QRFLOW_KEY }]

What the assistant can do

ToolWhat it doesScope
list_code_kindsEvery kind of code with its fields and the plan it needs. The assistant calls this before creating something unusual.profile
list_domainsYour link domains, the default, and their ids.domains:read
get_qr_imageA PNG the assistant can show or save, plus the SVG URL for print.codes:read
get_accountWho is signed in, plan, limits.profile

How it stays safe

  • The assistant only ever holds a token for your account, issued after you pressed Allow on a QRFLOW page. Disconnect it any time on Account › Connected apps.
  • Tokens are bound to the MCP server; they cannot be replayed against the REST API.
  • Every write goes through the same validation as the dashboard: allow-listed destinations, plan checks, fair-use ceilings.
  • Destructive tools describe themselves carefully: delete_qr_code tells the model to prefer pausing when a print exists.
  • Discovery documents live at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource; registration is RFC 7591; PKCE S256 only.

SDKs and the OpenAPI spec

  • TypeScript / JavaScript: npm install qrflow (npm). Zero dependencies, ESM and CommonJS, full types; runs in Node 18+, Bun, Deno and Workers. Retries 429s for you and ships verifyWebhook / parseWebhook.
  • Python 3.9+, standard library only: qrflow.py.
  • OpenAPI 3.1: /api/v1/openapi.json. Import into Postman or Insomnia, generate a client in any language, or attach it to a ChatGPT Action.
  • Both clients throw a typed error (QRFlowError with status, code, message, retryAfter) and ship a webhook verifier. The single-file TypeScript source is still at /sdk/qrflow.ts if you would rather vendor it.
TypeScriptPythonCalls
me()me()GET /me
catalog()catalog()GET /catalog
listCodes({ limit, q })list_codes(limit, q)GET /codes
getCode(id)get_code(id)GET /codes/:id
createCode(input)create_code(**fields)POST /codes
updateCode(id, patch)update_code(id, **patch)PATCH /codes/:id
deleteCode(id)delete_code(id)DELETE /codes/:id
makeDynamic(id)make_dynamic(id)POST /codes/:id/dynamic
scans(id, { from, to, group })scans(id, from_, to, group)GET /codes/:id/scans
bulkCreate(rows, colors)bulk_create(rows, **colors)POST /codes/bulk
domains()domains()GET /domains
listWebhooks() / createWebhook() / testWebhook(id) / deleteWebhook(id)list_webhooks() / create_webhook() / test_webhook(id) / delete_webhook(id)/webhooks
imageUrl(id, size)image_url(id, size)The image address (fetch it with the key)
verifyWebhook(raw, header, secret)verify_webhook(raw, header, secret)Signature check for deliveries

Endpoint reference

Base URL https://qrflow.codes/api/v1. Bodies and responses are JSON. Dates are ISO 8601 in UTC. Send only the fields that change on PATCH.

GETprofile

Plan, feature flags and the limits that apply to this account, plus the key's scopes.

curl https://qrflow.codes/api/v1/me \
  -H "Authorization: Bearer $QRFLOW_KEY"
{ "id": "…", "email": "ops@example.com", "plan": "business", "paid": true,
  "features": { "dynamic_codes": true, "custom_domain": true, "link_names": true, "gs1": true, "api_keys": true },
  "limits": { "saved_codes": 25000, "bulk_per_month": 10000, "bulk_per_request": 2000, "link_domains": 5, "requests_per_minute": 600 },
  "auth": "api_key", "scopes": ["codes:read", "codes:write"] }

GETpublic

Native types (url, wifi, vcard, email, phone, sms, text, location) and the 50-odd subtypes (Instagram, Google review, Wi-Fi, app store…), each with the fields it needs and the plan it requires. No key needed.

curl https://qrflow.codes/api/v1/catalog

GETcodes:read

Newest first.?limit= up to 100,?q= searches labels.

curl "https://qrflow.codes/api/v1/codes?limit=20&q=menu" \
  -H "Authorization: Bearer $QRFLOW_KEY"

POSTcodes:write

Same rules as Save on the site: url, phone, email, sms and location codes are dynamic on paid plans; Wi-Fi, vCard and text carry their content in the pattern. Put a subtype id in destination_data.subtype to make, say, a Google review code. Optional domain_id picks which of your link domains the code prints with (GET /domains lists them).

curl -X POST https://qrflow.codes/api/v1/codes \
  -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "url", "destination_data": { "url": "https://example.com/menu" }, "label": "Table tents", "frame_style": "caption-below", "frame_caption": "Scan for menu" }'
{ "code": { "id": "…", "label": "Table tents", "kind": "url", "dynamic": true, "short_code": "x7k2p9a",
  "short_url": "https://go.example.com/x7k2p9a", "scans": 0, "image_url": "https://qrflow.codes/api/v1/codes/…/image.svg", … } }

GETcodes:read

The code with its scan count, short link and image address.

curl https://qrflow.codes/api/v1/codes/$ID \
  -H "Authorization: Bearer $QRFLOW_KEY"

PATCHcodes:write

Any of: destination_data (dynamic codes only, the print stays valid), label, paused, expires_at (ISO or null), slug (a link name on your domain), domain_id (which of your link domains this code prints with; null = the account default), fg_color, bg_color, frame_style, frame_caption, frame_caption2. Send only the fields you are changing.

curl -X PATCH https://qrflow.codes/api/v1/codes/$ID \
  -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "destination_data": { "url": "https://example.com/menu-fall" }, "slug": "menu" }'

DELETEcodes:write

Gone for good, including its scan history. A dynamic code's printed link stops resolving. Prefer paused: true if the print is still out there.

curl -X DELETE https://qrflow.codes/api/v1/codes/$ID \
  -H "Authorization: Bearer $QRFLOW_KEY"
204 No Content

POSTcodes:write

The printed pattern changes (it now encodes the short link), so re-render the image afterwards.

curl -X POST https://qrflow.codes/api/v1/codes/$ID/dynamic \
  -H "Authorization: Bearer $QRFLOW_KEY"

GETcodes:read

Print-ready SVG with the frame and colors.?size= sets the module grid width in px; the file scales without loss anyway.

curl https://qrflow.codes/api/v1/codes/$ID/image.svg \
  -H "Authorization: Bearer $QRFLOW_KEY" -o code.svg

GETanalytics:read

?from= and?to= (ISO dates, up to 92 days, default the last 30) and?group= day, device, country, city, browser, os or referrer. The same numbers as the analytics page.

curl "https://qrflow.codes/api/v1/codes/$ID/scans?from=2026-09-01&to=2026-09-21&group=day" \
  -H "Authorization: Bearer $QRFLOW_KEY"
{ "code_id": "…", "from": "…", "to": "…", "group": "day", "total": 412,
  "rows": [ { "key": "2026-09-01", "scans": 18 }, { "key": "2026-09-02", "scans": 25 }, … ] }

POSTcodes:write

Up to 2,000 URL codes in one call, all dynamic. Counts against the same monthly bulk allowance as the Bulk page (10,000 on Business). Rows that are not web addresses come back in rejected; the rest are created.

curl -X POST https://qrflow.codes/api/v1/codes/bulk \
  -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "destination": "https://example.com/t/1", "label": "Table 1" }, { "destination": "https://example.com/t/2", "label": "Table 2" } ] }'
{ "codes": [ … ], "rejected": [], "remaining_this_month": 9998 }

GETdomains:read

Your connected domains, their status, and which one dynamic codes print with by default (default_base). Pass a domain's id as domain_id on a code to print that code with a different one.

curl https://qrflow.codes/api/v1/domains \
  -H "Authorization: Bearer $QRFLOW_KEY"

GETwebhooks:manage

Your webhooks with their events, last status and failure count.

curl https://qrflow.codes/api/v1/webhooks \
  -H "Authorization: Bearer $QRFLOW_KEY"

POSTwebhooks:manage

url must be https on a public host; events is any of scan, code.created, code.updated, code.deleted. The signing secret is returned once.

curl -X POST https://qrflow.codes/api/v1/webhooks \
  -H "Authorization: Bearer $QRFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/qrflow", "events": ["scan", "code.updated"] }'
{ "webhook": { "id": "…", "url": "…", "events": ["scan", "code.updated"], "active": true, "secret": "whsec_…" } }

POSTwebhooks:manage

Sends a signed ping now and reports the response.

curl -X POST https://qrflow.codes/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $QRFLOW_KEY"

DELETEwebhooks:manage

Stops all deliveries, including queued retries.

curl -X DELETE https://qrflow.codes/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $QRFLOW_KEY"
204 No Content

GETpublic

Frame ids and what each needs (caption, second line), grouped like the customizer. No key needed.

curl https://qrflow.codes/api/v1/frames

The Code object

Every endpoint that touches a code returns the same shape. Ignore fields you do not know; new ones are added over time.

FieldTypeMeaning
iduuidStable id. Use it in every other call.
labelstring | nullThe name in the dashboard. Up to 120 characters. Searchable with?q=.
kindstringThe catalog id: url, wifi, instagram, googlereview,... kind_label is the human name.
typestringThe encoding: url, text, wifi, vcard, email, phone, sms, location.
destination_dataobjectThe fields you sent (plus subtype for kinds). destination is a one-line summary of it.
dynamicbooleanTrue when scans go through QRFLOW and the destination can change. dynamic_capable says whether this type could be dynamic on a paid plan.
short_codestringSeven characters, never changes.
short_urlstringWhat to print for a dynamic code. Includes your domain and slug when set.
domain_iduuid | nullWhich link domain this code prints with; null means the account default.
fg_color, bg_colorhexModule and background colors.
has_logobooleanA logo was added in the dashboard; image.svg includes it.
frame_style, frame_caption, frame_caption2string | nullFrame id and captions, as in the customizer.
scansintegerLifetime scan count.
created_at, updated_atISO 8601UTC.
manage_urlurlThe code's page in the dashboard, for a 'Open in QRFLOW' link.
image_urlurlGET /codes/:id/image.svg. Needs the Authorization header; not a public image URL.
svg_download_url, png_download_urlurlThe same SVG (frame, colors, logo) and a plain PNG through signed links that work for 24 hours with no header: for <img> tags, scripts, and assistants saving a file. download_expires_at says when they lapse; any read of the code returns fresh ones.

Other shapes: Scans (code_id, from, to, group, total, rows[key, scans]), Domain (id, host, status, active, is_default, verified_at, grace_until), Webhook (id, url, events, active, last_status, last_delivery_at, consecutive_failures, plus secret once), Me (id, email, plan, paid, features, limits, auth, scopes) and Error (error, message). The OpenAPI document has every property typed.

Business

Webhooks

QRFLOW calls your https URL when something happens. Create one on Account › Webhooks or with POST /webhooks; you get the signing secret once. Up to 10 per account.

EventWhendata
scanBatched: every few minutes, all new scans since the last delivery, up to 500 per call.count, from, to, scans[] with code_id, label, short_code, slug, scanned_at, device, country, city, referrer, browser, os, language
code.createdAt once from the API and assistants; within minutes from the dashboard. A bulk request sends one event with bulk: true and codes[].code, source
code.updatedSame timing. Covers destination, label, pause, expiry, link name, domain, colors, frame, and conversion to dynamic.code, changed[] (the field names that changed)
code.deletedSame timing.code: { id, label, short_code, slug }
pingWhen you press Test.webhook_id, message

What arrives

Every payload is { id, event, created_at, data }. id is stable across retries of one delivery, so you can dedupe on it.

{
  "id": "9b1c6d2e-…",
  "event": "scan",
  "created_at": "2026-09-21T18:05:00.000Z",
  "data": {
    "count": 2,
    "from": "2026-09-21T18:00:00.000Z",
    "to": "2026-09-21T18:04:12.331Z",
    "scans": [
      { "code_id": "…", "label": "Table tents", "short_code": "x7k2p9a", "slug": "menu", "scanned_at": "2026-09-21T18:03:40.101Z",
        "device": "mobile", "country": "US", "city": "Las Vegas", "referrer": null, "browser": "Safari", "os": "iOS", "language": "en-US" },
      { "code_id": "…", "label": "Table tents", "short_code": "x7k2p9a", "slug": "menu", "scanned_at": "2026-09-21T18:04:12.331Z",
        "device": "mobile", "country": "MX", "city": "Tijuana", "referrer": null, "browser": "Chrome", "os": "Android", "language": "es-MX" }
    ]
  }
}
{
  "id": "2f0a…",
  "event": "code.updated",
  "created_at": "2026-09-21T18:06:00.000Z",
  "data": {
    "code": { "id": "…", "label": "Table tents", "kind": "url", "dynamic": true, "short_url": "https://go.example.com/menu", "scans": 412, "…": "…" },
    "changed": ["destination_data"]
  }
}
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: QRFLOW-Webhooks/1.0 (+https://qrflow.codes/developers)
X-QRFLOW-Event: scan
X-QRFLOW-Signature: t=1758477900,v1=5f1c…e9

Verify the signature

X-QRFLOW-Signature: t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256 over ${t}.${rawBody} with your secret and compare to v1 in constant time; reject if t is more than five minutes old. Use the raw bytes you received, never a re-serialized object. Working receivers for Next.js and Python are in the recipes, and both SDKs include the helper.

Delivery rules

  • Answer any 2xx within 8 seconds. Do the work after you respond.
  • Anything else is retried after 1, 5, 15, 60, 240 and 720 minutes.
  • Twenty consecutive failures switch the webhook off and email the account owner. Turn it back on once the receiver is fixed; queued retries resume.
  • URLs must be https on a public host. localhost, private ranges and qrflow.codes itself are refused. Use a tunnel while developing.
  • Press Test to receive a signed ping and see the status your server answered.

Errors

Every error is { "error": "<code>", "message": "<what to do>" } with the status below. The message is written for a person; show it.

StatuserrorMeaning

Symptom, cause, fix

Troubleshooting

401 invalid_token on every call

WhyThe header is wrong or the key is not live.

FixSend exactly Authorization: Bearer qrf_live_… (a space, not a colon). Check the key was not revoked on Account › API keys. If you copied it from a chat or a doc, watch for a trailing period or a smart quote.

401 on /api/v1 with a token that works on the MCP server

WhyTokens minted for https://qrflow.codes/mcp are bound to it.

FixUse an API key for REST, or run a second OAuth flow without resource= to get a token for the REST API.

402 upgrade_required when creating a key or a webhook

WhyBoth are Business features.

FixUpgrade on /pricing, or use the MCP server, which works on every plan through sign-in.

402 on POST /codes from an OAuth app

WhyThe user's plan does not include what the app asked for (a dynamic code, a domain).

FixRead GET /me features first and adapt: create the code anyway (it will be static on Free) or tell the user what the plan needs.

403 insufficient_scope

WhyScopes are fixed when a key is made.

FixMake a new key with the scopes you need and revoke the old one. For OAuth, ask for the scope in the authorize request.

400 not_dynamic when I PATCH destination_data

WhyThe code is static: made on Free, or a Wi-Fi/vCard/text type.

FixFor url/phone/email/sms/location on a paid plan, POST /codes/:id/dynamic, then re-download and re-print (the picture changes). Wi-Fi, vCard and text can never be dynamic; make a url code that opens a page instead.

400 no_domain when I set slug

WhyLink names live on your domain.

FixConnect and verify a domain on the Account page first. On qrflow.codes/q the path is always the short_code.

409 conflict on slug

WhyAnother of your codes has that name.

FixGET /codes?q= to find it, or pick another name. Names are per account, not global.

CORS error in the browser console

WhyThe API only accepts server-to-server calls (and Canva). This is deliberate: a key in a web page is a leaked key.

FixMove the call into a route handler, server action, edge function or backend and call that from the page.

The image shows a QRFLOW watermark

WhyThe account is on the Free plan.

FixPaid plans remove it. Free is meant for the site's own generator.

short_url still says qrflow.codes/q/… after I added my domain

WhyThe domain is not verified yet, or its CNAME is wrong.

FixCheck the status on the Account page or GET /domains (status must be verified). Existing codes switch automatically once it is.

image_url gives 401 in an <img> tag

WhyIt needs the Authorization header, which an <img> cannot send.

FixUse svg_download_url or png_download_url from the same Code object: signed links that work for 24 hours without a header. For something permanent, proxy image_url through your server (see the Next.js recipe) or encode short_url yourself.

I need a PNG, not an SVG

WhyThe SVG carries frame and logo; the PNG is the plain code.

FixGET /codes/:id/image.png (bearer or the signed png_download_url) returns a PNG, 256 to 2048 px. For a PNG with the frame, convert the SVG with sharp or resvg (sharp(svgBuffer).png().toBuffer()).

My assistant said the image endpoint refused it and drew the code itself

WhyIt fetched image_url, which needs a bearer.

FixEvery code now carries png_download_url and svg_download_url, and get_qr_image returns them; the assistant can curl those with no sign-in. A locally drawn code that encodes the same short_url still works and still counts scans, but it lacks the frame and logo.

Webhook never arrives

WhyURL rules or the receiver.

FixThe URL must be https on a public host (no localhost, no private IPs, not qrflow.codes). Press Test on the webhook: the result shows the status your server answered. Scan events are batched and can take up to about five minutes; code.* events from the dashboard also queue for a few minutes, while API and MCP writes deliver at once.

Signature never verifies

WhyYou signed a re-serialized body.

FixVerify against the exact raw bytes you received, before JSON parsing. In Express use express.raw({ type: 'application/json' }) on that route; in Next.js App Router use await req.text(); in Flask request.get_data(). Then compute HMAC-SHA256 of `${t}.${raw}`.

Webhook switched itself off

WhyTwenty consecutive failures.

FixFix the receiver, then set it active again (Account › Webhooks, or delete and recreate). You were emailed when it happened. Queued retries resume.

Duplicate webhook deliveries

WhyA slow 2xx (over 8 seconds) counts as a failure and is retried.

FixRespond first, process after. Dedupe on the payload's id, which is stable across retries.

429 rate_limited during an import

Why600 requests a minute per key.

FixUse POST /codes/bulk (2,000 codes in one request) instead of one POST per code, or sleep for Retry-After seconds.

I deleted a code and the printed poster now shows 'Code not found'

WhyDelete is permanent and kills the link.

FixThere is no undo. Next time PATCH { paused: true }; a paused code shows a friendly page and can be resumed.

My key stopped working after I downgraded

WhyKeys keep working 30 days after leaving Business, then answer 402.

FixRe-subscribe; nothing was deleted, and the same keys work again.

Something not on the list? hello@qrflow.codes, with the exact request and the JSON error you got back. Business accounts get priority.

Limits and fair use

Limit
Requests per minute, per key600. 429 with Retry-After beyond that.
Keys per account10
Webhooks per account10
Saved codes (fair use)1,000 Premium, 25,000 Business; the API stops at twice that.
Bulk500/month (500 per request) Premium; 10,000/month (2,000 per request) Business.
Link domains1 Premium, 5 Business
ScansUnlimited. A check-in email at 100,000 scans per code per month on Premium, 1,000,000 on Business; nothing is throttled.
Scan analytics window92 days per request
List page size100 (?limit=)
Label / captions / slug120 / 60 / 40 characters
Webhook timeout and retries8 seconds; retries after 1, 5, 15, 60, 240 and 720 minutes; off after 20 consecutive failures.
After leaving BusinessKeys and webhooks keep working 30 days, then 402. Nothing is deleted.

Fair use is what the plan is priced for. Nothing is throttled at the number; the API stops at twice it and a person emails you first. Higher volumes: hello@qrflow.codes.

Security, for you and for the people who scan

How QRFLOW protects your account

  • Keys are shown once and stored as SHA-256 hashes. Nobody at QRFLOW can read a key back; if you lose it, make a new one.
  • Every request is scoped to the account the key belongs to. A code id from another account is a 404, never a leak.
  • Scopes are fixed per key, so a key for a reporting dashboard cannot create or delete codes.
  • 600 requests a minute per key; over that is a clean 429, not a slowdown for everyone.
  • Destinations are allow-listed: http, https, mailto, tel, sms, geo and a short list of app schemes (whatsapp, tg, signal, spotify, app stores). javascript:, data: and file: are rejected at create time, so a compromised integration cannot turn your codes into an attack.
  • Webhook URLs must be https on public hosts; QRFLOW never calls private networks or itself. Every delivery is signed and every payload has a stable id.
  • OAuth clients register with PKCE S256 and tokens are bound to the server they were issued for; a token for the MCP server cannot be replayed on the REST API.
  • Anyone can revoke a key or disconnect an app on the Account page; the effect is immediate.

What to do on your side

  • Environment variables, never source code. If a key lands in a git history, revoke it.
  • Server-side only. The API refuses browser origins, but your own endpoints that wrap it need authentication too, or anyone can make codes on your bill.
  • Give each integration its own key with the scopes it needs, named after the integration. Revoking one then affects one thing.
  • Verify webhook signatures and reject timestamps older than five minutes.
  • If your users' data goes into labels or destinations, remember QRFLOW stores them; keep personal data out of labels where you can.

What scans record, and what they do not

  • Each redirect stores device type, country, city, referrer, browser, operating system and language, derived from the request, and a one-way hash of code + day + IP + user agent so the owner can count unique visitors. The hash cannot be turned back into an address.
  • No cookies are set on the person who scans and the IP address itself is not kept with the scan. Known bots and link-preview crawlers are skipped.
  • Deleting a code deletes its scans. Deleting the account deletes everything.
  • Full text: https://qrflow.codes/privacy and https://qrflow.codes/terms.

Versioning and stability

The API is versioned in the path: /api/v1. Within v1 we add fields, endpoints, kinds and events; we do not remove or rename anything, and unknown fields in responses should be ignored by your code.

If a change ever has to break v1, it ships as /api/v2 and v1 keeps running for at least twelve months. Key owners are emailed about deprecations 90 days ahead.

The MCP server follows the same rule for its tools: arguments are only added, and every tool keeps its name.

The OpenAPI document at /api/v1/openapi.json and the Markdown at /llms-full.txt are generated from the code that serves the API, so they describe what is live today.

Questions developers ask

Do I have to pay to use the QRFLOW API?

API keys come with the Business plan, $29 a month, month to month. The MCP server (Claude, ChatGPT, Cursor, Claude Code) and OAuth for your own app work on every plan through sign-in, and what they may create follows the plan. GET /catalog, GET /frames and /preview.svg need no key at all.

Can I generate QR codes for free through the API?

Not with a key. For a plain static image, the free generator at qrflow.codes or any open-source QR library does the job. The API is for dynamic codes, your own domain, analytics, bulk and webhooks, which are what a paid account is.

Can I call the API from the browser?

No. It refuses browser origins so a key can never end up in a web page. Call it from a route handler, server action, edge function or backend, and call that from your page.

What image formats do I get?

SVG with your colors, frame, captions and logo (GET /codes/:id/image.svg, 256 to 4096 px nominal) and a plain PNG (GET /codes/:id/image.png, 256 to 2048 px). Both take a bearer, or the signed svg_download_url / png_download_url every Code object carries, which work for 24 hours with no header. The MCP tool get_qr_image returns the PNG inline plus both links.

Can I change a QR code after it is printed?

Yes, if it is dynamic (url, phone, email, sms, location on a paid plan). PATCH /codes/:id with a new destination_data; the picture does not change, the next scan goes to the new place. Wi-Fi, vCard and text codes carry their content in the picture and cannot change.

What is the difference between short_url and the destination?

short_url is the link inside the picture (go.example.com/menu). The destination is where that link redirects (https://example.com/menu-fall). You print short_url once and change the destination as often as you like.

Can codes use my own domain?

Yes. Premium connects 1 domain, Business 5; you add a CNAME and verify on the Account page. Business picks a domain per code with domain_id. Link names (slug) make readable paths on it.

What does a scan record about the person scanning?

Device type, country, city, referrer, browser, operating system and language, from the request, plus a one-way daily hash for unique-visitor counts. No cookies, and the IP address is not stored. Enough for a chart, not enough to identify anyone. Details: https://qrflow.codes/privacy#scans

Is there an npm or PyPI package?

npm: `npm install qrflow` (https://www.npmjs.com/package/qrflow), zero dependencies, ESM and CommonJS, full TypeScript types, runs in Node 18+, Bun, Deno and Workers; it wraps every endpoint, retries 429s, and includes verifyWebhook/parseWebhook. Python: a single-file client at https://qrflow.codes/sdk/qrflow.py (standard library only) with verify_webhook; a PyPI package will follow.

Does it work with Claude, ChatGPT, Cursor and Claude Code?

Yes. QRFLOW is an MCP server at https://qrflow.codes/mcp. Add it as a connector, sign in once, and ask in plain words. Eleven tools cover creating, editing, pausing, naming, bulk, analytics, images and domains.

Can my own users connect their QRFLOW accounts to my app?

Yes, with OAuth 2.0. Register a client at /api/oauth/register (no account needed), send users to /oauth/authorize with PKCE, and call the API with their token. Codes land in their account under their plan.

How do I test webhooks on localhost?

Expose your dev server with a tunnel (cloudflared or ngrok) and use its https address as the webhook URL, then press Test on Account › Webhooks to receive a signed ping. Webhook URLs must be public https; localhost and private addresses are refused.

What happens to my integration if I cancel Business?

Keys and webhooks keep working for 30 days, then answer 402. Codes, scans and domains stay in the account. Re-subscribing turns everything back on with the same keys.

How do I make a Google review, Instagram, Wi-Fi or PDF code through the API?

GET /catalog lists every kind with its fields. Then POST /codes with the kind's type and fields, adding subtype for kinds: { type: 'url', destination_data: { subtype: 'googlereview', placeId: 'ChIJ…' } }, { type: 'wifi', destination_data: { ssid, password, encryption: 'WPA' } }, { type: 'url', destination_data: { subtype: 'instagram', handle: 'acme' } }.

Can the API upload a logo onto a code?

Not yet. Add the logo in the dashboard; image.svg includes it and has_logo tells you it is there. Colors, frames and captions are all settable through the API.

Is the API stable?

v1 only adds; it never removes or renames. A breaking change would ship as v2 with v1 kept alive for at least twelve months and 90 days' notice by email.

For agents, tools and the assistants reading this

Machine-readable

Everything on this page exists in a form software can fetch. All of it is generated from the code that serves the API, so it is never out of date.

URLWhat it is
llms-full.txthttps://qrflow.codes/llms-full.txtThis whole reference as Markdown: concepts, every endpoint, every MCP tool, recipes, troubleshooting, FAQ. Generated from the same source as this page.
developers.mdhttps://qrflow.codes/developers.mdSame document, for tools that fetch a.md.
llms.txthttps://qrflow.codes/llms.txtThe site index for assistants, pointing here.
The API in one pagehttps://qrflow.codes/qr-code-apiWhat the API does, when a client-side library is the better answer, and what it costs. The short version of this reference, for deciding rather than building.
openapi.jsonhttps://qrflow.codes/api/v1/openapi.jsonOpenAPI 3.1. Import into Postman, Insomnia, a code generator, or a ChatGPT Action.
MCP serverhttps://qrflow.codes/mcpStreamable HTTP, OAuth with dynamic registration or a Business key as bearer.
server.jsonhttps://qrflow.codes/.well-known/mcp/server.jsonThe MCP registry manifest.
OAuth discoveryhttps://qrflow.codes/.well-known/oauth-authorization-serverRFC 8414 metadata; the protected-resource document is next to it.
npm packagehttps://www.npmjs.com/package/qrflownpm install qrflow. Typed client, zero dependencies, webhook verification. Python single-file client at https://qrflow.codes/sdk/qrflow.py.
GitHubhttps://github.com/nativecodeapps/qrflow-sdkClients, OpenAPI snapshot and runnable examples for Next.js, Workers, Express, FastAPI and Flask webhooks. Issues and PRs welcome.

Questions, ideas, a kind of code we should add: hello@qrflow.codes. Terms: /terms. Privacy: /privacy.