OpenQR

Developers

How to add QR codes to your app with the free OpenQR API

If your app needs to put a QR code on an invoice, a ticket or a receipt, you have two options: ship a client-side library and wire up canvas rendering, or make one HTTP request and get back a finished image. This guide covers the second. The OpenQR REST API renders QR codes server-side over a single endpoint — no npm dependency, no browser, no build step — and it is free with an account.

9 min read · Updated 26 June 2026

Why call an API instead of bundling a library

QR generation libraries are fine until they are not. They add weight to your bundle, they only run where your code runs (a client lib cannot help a cron job, a PDF worker or a transactional email), and you end up owning the rendering quirks — module sizing, quiet zones, SVG vs raster output — yourself. An API moves all of that behind one URL. You send the data, you get back an image/svg+xml or image/png response, and you embed it wherever you need it: a server-rendered page, a PDF, an email, a print job.

  • No client dependency — nothing to install, version or audit; the rendering lives on OpenQR's side.
  • Works anywhere your server runs — request a code from a background job, a webhook handler or a serverless function.
  • Consistent output — the same endpoint produces the same image whether you are on Node, Python, Go or shell.
  • Vector or raster on demand — ask for SVG when you need crisp print scaling, PNG when a tool only accepts bitmaps.

Static codes, generated for you

The /v1/qr endpoint produces static QR codes: the data is encoded directly into the image, so the code never expires and needs no server to resolve. If you instead need an editable code you can repoint after printing, that is a dynamic code — see our guide to the dynamic codes API.

Get a free API key

Every API call needs a key. Go to openqr.uk/api, sign in with a magic link (enter your email, click the link — no password), then create a key under the dashboard. It is shown once and stored only as a SHA-256 hash on our side, so copy it somewhere safe. Keys look like oqr_… and travel as a bearer token.

Why is the API key-gated when the tool is free?

The in-browser generator stays free and needs no signup. The API is deliberately tied to an account so we can keep abuse off the network — that is the trade-off that lets everything, including the API itself, stay free. Keep your key on the server; never ship it to a browser.

bash
# A key authenticates every request as a bearer token:
Authorization: Bearer oqr_YOUR_API_KEY

The /v1/qr image endpoint

There is one endpoint for rendering images: /v1/qr. It accepts both GET (parameters in the query string) and POST (parameters in a JSON body) and returns the rendered image directly — not JSON wrapping a URL, the actual bytes. Use GET when you want a URL you can drop straight into an <img> tag or a fetch; use POST when your data is long or awkward to URL-encode.

ParameterTypeDefaultNotes
datastring (required)Text or URL to encode. Max 2000 characters.
formatsvg | pngsvgSVG is vector; PNG is raster.
sizeinteger51264–2048. Pixel size for PNG, viewBox for SVG.
margininteger40–16. The quiet zone in modules around the code.
darkhexForeground colour, e.g. 232E3A (no #). SVG only.
lighthexBackground colour, e.g. FFFFFF (no #). SVG only.

Colour customisation applies to SVG

The dark and light parameters recolour SVG output. PNG output is rendered in the default black-on-white. If you need a coloured PNG, request the SVG and rasterise it yourself, or keep PNG for cases where default colours are fine.

SVG vs PNG — when to use each

Reach for SVG by default. It is resolution-independent, so it stays razor-sharp at any print size, it is tiny over the wire, and you can inline it directly into HTML or a PDF without a second request. Reach for PNG when a downstream tool only accepts raster images — some email clients, certain PDF libraries, image-only upload fields, or anywhere you need a fixed-pixel file on disk. For anything you control end to end, SVG is the better engineering choice.

Quickstart

Three ways to render the same code — a URL encoded as a 600px PNG. Note the key is read from an environment variable in every example; do not hard-code it.

bash
# GET: parameters in the query string, image written to a file
curl "https://openqr.uk/v1/qr?data=https%3A%2F%2Fexample.com&format=png&size=600" \
  -H "Authorization: Bearer $OPENQR_KEY" \
  --output qr.png

# POST: parameters as JSON (handy when data is long)
curl -X POST https://openqr.uk/v1/qr \
  -H "Authorization: Bearer $OPENQR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":"https://example.com","format":"png","size":600}' \
  --output qr.png
javascript
import { writeFile } from "node:fs/promises";

const res = await fetch("https://openqr.uk/v1/qr", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENQR_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ data: "https://example.com", format: "png", size: 600 }),
});

if (!res.ok) {
  const { error } = await res.json(); // errors come back as JSON
  throw new Error(`OpenQR ${res.status}: ${error}`);
}

const png = Buffer.from(await res.arrayBuffer());
await writeFile("qr.png", png);
python
import os
import requests

res = requests.get(
    "https://openqr.uk/v1/qr",
    params={"data": "https://example.com", "format": "png", "size": 600},
    headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
)
res.raise_for_status()  # raises on 4xx/5xx; body is {"error": "..."}

with open("qr.png", "wb") as f:
    f.write(res.content)

Authentication and error handling

On success you get the image bytes with the matching content type. On failure you get JSON in the shape { "error": "…" } with a meaningful status code, so check the status before reading the body as an image.

StatusMeaningFix
200Image renderedRead the body as bytes (it is the image).
400Bad requestdata is missing, over 2000 chars, or the JSON body is invalid.
401UnauthorizedMissing or wrong key — send Authorization: Bearer oqr_…

Rate limits

Image generation is free with generous limits — fine for normal app traffic. Creating dynamic codes is rate-limited separately (20 per hour per account, and bulk creation is capped at 200 per request). If you ever hit a limit you will get a 429 with an error message; back off and retry. CORS is open, so you can call the API from anywhere — but keep the key server-side regardless.

Worked example: a QR on a server-rendered receipt

Here is the on-ramp made concrete. Say you render receipts or invoices on the server and want a "scan to pay" (or "scan to view") QR on each one. The cleanest approach is to fetch the code as SVG and inline it into the HTML: no extra image request from the browser, and it scales perfectly when the customer prints the page or you turn it into a PDF.

This is a Next.js App Router route handler, but the shape is the same in Express or any server framework — fetch the SVG, drop it into the markup.

javascript
// app/invoices/[id]/receipt/route.ts
import { NextResponse } from "next/server";
import { getInvoice } from "@/lib/invoices";

async function qrSvg(data: string): Promise<string> {
  const url =
    "https://openqr.uk/v1/qr?" +
    new URLSearchParams({ data, format: "svg", size: "240", margin: "2" });

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.OPENQR_KEY!}` },
    // The QR for a given URL never changes, so let it be cached.
    cache: "force-cache",
  });

  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`OpenQR ${res.status}: ${error}`);
  }
  return res.text(); // raw <svg>…</svg>
}

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const invoice = await getInvoice(id);
  const payUrl = `https://pay.example.com/i/${invoice.id}`;
  const svg = await qrSvg(payUrl);

  const html = `<!doctype html>
<html lang="en">
  <body>
    <h1>Receipt ${invoice.number}</h1>
    <p>Amount due: £${invoice.total.toFixed(2)}</p>
    <figure style="width:240px">
      ${svg}
      <figcaption>Scan to pay</figcaption>
    </figure>
  </body>
</html>`;

  return new NextResponse(html, {
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

If your pipeline produces PDFs and your PDF tool wants a bitmap, swap two things: request format=png, read the response as bytes, and pass a data URI to the renderer.

javascript
async function qrPngDataUri(data: string): Promise<string> {
  const url =
    "https://openqr.uk/v1/qr?" +
    new URLSearchParams({ data, format: "png", size: "480" });

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.OPENQR_KEY!}` },
  });
  if (!res.ok) throw new Error(`OpenQR ${res.status}: ${(await res.json()).error}`);

  const b64 = Buffer.from(await res.arrayBuffer()).toString("base64");
  return `data:image/png;base64,${b64}`; // <img src="…"> for your PDF lib
}

Cache the image, not the request

The QR for a fixed URL is deterministic, so you only ever need to generate it once. Cache the SVG (or PNG) by its data string — in memory, on disk, or with HTTP caching as above — and you will rarely touch the API after the first render. For tickets with a unique URL each, key the cache on the ticket id.

Import the whole API with the OpenAPI spec

You do not have to read endpoints from a doc and hand-write clients. OpenQR publishes a full OpenAPI 3.1 description at openqr.uk/openapi.json. Import it into Postman or Insomnia to get a ready-made request collection, point your code generator at it to produce a typed client, or hand it to an AI coding agent so it knows every parameter and response shape. It is the fastest way to go from "getting started" to a working integration.

When you are ready to go beyond static images: dynamic codes let you repoint a printed code and read scan analytics over the same API, and the OpenQR MCP server exposes the whole thing to AI agents over a single endpoint. If you are still deciding which kind of code you need, our explainer on static vs dynamic QR codes covers the trade-offs.

Get your free API keySign in with a magic link, create a key, and call /v1/qr in minutes — free, no card.
Yes. Generating QR images and creating dynamic codes are free with an account. The API is key-gated so we can keep abuse off the network, but there is no charge and no card required — the in-browser generator stays free and no-signup too.

Related reading