Developers
How to generate QR codes in Python
By Sam Moreton · updated 30 June 2026
The usual advice for QR codes in Python is to pip install qrcode and render locally. That is genuinely fine for a script on your laptop. But when you want zero dependencies, identical output across every service, and SVG or PNG on demand from the same code path you already use in Node or Go, an HTTP call is the cleaner option. This guide shows how to generate QR codes in Python by calling the free OpenQR REST API with the requests library — one request in, finished image out.
9 min read · Updated 30 June 2026
API vs a local library — be honest about the trade-off
Local libraries like qrcode (Pillow-backed) and segno are good tools. If you are writing a one-off script, generating codes fully offline, or you specifically want no network dependency, install one and move on — there is no shame in it. The case for an API is different: it is about consistency and reach, not raw QR generation.
- Zero dependencies — no Pillow, no native build wheels, nothing to pin or audit. If your environment already has
requests(or even just the stdlib), you are done. - Consistent server-side output — the same endpoint returns the same image whether it runs in a web request, a Celery task, a cron job or a Lambda. No per-environment rendering quirks.
- SVG or PNG on demand — ask for vector when you need crisp print scaling, raster when a tool only accepts bitmaps. No second library for the other format.
- One code path across languages — your Python service and your Node service hit the same URL with the same parameters, so the QR looks identical everywhere.
When a local library is the right call
If you need fully offline generation, have no outbound network, or are rendering millions of codes where a round-trip per code matters, use a local library — segno is excellent and dependency-light. The API shines when you value consistency, no install footprint, and shared output across services over avoiding a network hop.
Get a free API key
Every 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 in 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 the API is key-gated when the tool is free
The in-browser generator is free and needs no signup. The API is tied to an account so we can keep abuse off the network — that is the trade-off that lets the API itself stay free. Keep your key on the server; never ship it to a browser or commit it to git.
Read the key from an environment variable
Never hard-code the key in your source. Export it into the environment (or a .env file loaded by your process manager) and read it with os.environ. The examples below all assume OPENQR_KEY is set.
# Set it in your shell, .env, or your deploy platform's secrets manager.
export OPENQR_KEY="oqr_YOUR_API_KEY"The /v1/qr image endpoint
There is one endpoint for rendering images: https://openqr.uk/v1/qr. It accepts GET (parameters in the query string) and POST (parameters in a JSON body), and returns the rendered image directly — the raw image/svg+xml or image/png bytes, not JSON wrapping a URL. Use GET for short data; use POST when your data is long or awkward to URL-encode.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| data | string (required) | — | Text or URL to encode. Max 2000 characters. |
| format | svg | png | svg | SVG is vector; PNG is raster. |
| size | integer | 512 | 64–2048. Pixel size for PNG, viewBox for SVG. |
| margin | integer | 4 | 0–16. The quiet zone in modules around the code. |
| dark | hex | — | Foreground colour, e.g. 232E3A (no #). SVG only. |
| light | hex | — | Background colour, e.g. FFFFFF (no #). SVG only. |
| theme | string | — | A saved theme's id or name. Applies the theme's colours and margin. |
Minimal example: save a PNG
The shortest useful program. A GET request with the data in the query string, the key read from the environment, and the response bytes written straight to disk. requests URL-encodes the params for you.
import os
import requests
resp = requests.get(
"https://openqr.uk/v1/qr",
params={"data": "https://example.com", "format": "png", "size": 600},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
resp.raise_for_status() # raise on 4xx/5xx before we trust the body
with open("qr.png", "wb") as f:
f.write(resp.content) # resp.content is the raw image bytesAlways check the status before saving
On success you get image bytes; on failure you get JSON like { "error": "…" } with a 400 or 401 status. resp.raise_for_status() turns a non-200 into an exception so you never write an error page to qr.png. If you prefer to handle it yourself, branch on resp.status_code.
POST with a JSON body for long data
When your data is long — a vCard, a wifi string, a URL stuffed with query parameters — a query string gets unwieldy. Send a POST with a JSON body instead. The parameters are identical; only the transport changes.
import os
import requests
payload = {
"data": "WIFI:T:WPA;S:My Network;P:super-secret-password;;",
"format": "png",
"size": 800,
"margin": 2,
}
resp = requests.post(
"https://openqr.uk/v1/qr",
json=payload, # sets Content-Type: application/json for you
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
resp.raise_for_status()
with open("wifi.png", "wb") as f:
f.write(resp.content)Render SVG instead of PNG
Ask for format=svg and the response is text — the raw <svg>…</svg> markup. SVG is resolution-independent, so it stays sharp at any print size, it is tiny over the wire, and you can inline it straight into HTML or a PDF with no second request. It is the better default for anything you control end to end. Note that the dark and light colour parameters apply to SVG output.
import os
import requests
resp = requests.get(
"https://openqr.uk/v1/qr",
params={
"data": "https://example.com",
"format": "svg",
"size": 480,
"dark": "232E3A", # foreground hex, no '#'
"light": "FFFFFF", # background hex, no '#'
},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
resp.raise_for_status()
svg = resp.text # the raw <svg> markup
with open("qr.svg", "w", encoding="utf-8") as f:
f.write(svg)Embedding in a web response (Flask / Django)
Saving to disk is only one option. In a web app you usually want to stream the image straight back, or inline an SVG into a template. Here is a Flask route that proxies a PNG, and a Django view that does the same — both fetch on demand and return the bytes with the right content type.
# Flask: stream a PNG back to the browser
import os
import requests
from flask import Flask, Response, request, abort
app = Flask(__name__)
@app.get("/qr.png")
def qr_png():
data = request.args.get("data", "")
if not data:
abort(400, "missing 'data'")
upstream = requests.get(
"https://openqr.uk/v1/qr",
params={"data": data, "format": "png", "size": 512},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
if upstream.status_code != 200:
abort(502, "QR service error")
return Response(upstream.content, mimetype="image/png")# Django: inline an SVG into the rendered page
import os
import requests
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.safestring import mark_safe
def receipt(request, invoice_id):
pay_url = f"https://pay.example.com/i/{invoice_id}"
resp = requests.get(
"https://openqr.uk/v1/qr",
params={"data": pay_url, "format": "svg", "size": 240, "margin": 2},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
if resp.status_code != 200:
return HttpResponseBadRequest("Could not render QR")
# mark_safe because we trust our own API's SVG output
html = f"<figure style='width:240px'>{mark_safe(resp.text)}"
html += "<figcaption>Scan to pay</figcaption></figure>"
return HttpResponse(html)Cache deterministic codes
The QR for a fixed data string never changes, so you only need to generate it once. Cache the bytes by their data string — in memory, on disk, or behind your CDN — and you will rarely touch the API after the first render. For per-record codes (a unique URL each), key the cache on the record id.
Embedding in a PDF or email
For a generated PDF or an HTML email, most renderers want a raster image, often as a base64 data URI. Fetch a PNG, encode it, and drop it into an <img> tag — no temp file needed.
import os
import base64
import requests
def qr_png_data_uri(data: str) -> str:
resp = requests.get(
"https://openqr.uk/v1/qr",
params={"data": data, "format": "png", "size": 480},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
resp.raise_for_status()
b64 = base64.b64encode(resp.content).decode("ascii")
return f"data:image/png;base64,{b64}"
# Use it in an email or PDF template:
img_tag = f'<img src="{qr_png_data_uri("https://example.com")}" alt="QR code">'Error handling that does not bite you
The endpoint returns image bytes on success and a JSON error body on failure, so the one mistake to avoid is reading the body as an image before checking the status. resp.raise_for_status() is the simplest guard; if you want a useful message, read the JSON on failure.
import os
import requests
def make_qr(data: str, fmt: str = "png", size: int = 512) -> bytes:
resp = requests.get(
"https://openqr.uk/v1/qr",
params={"data": data, "format": fmt, "size": size},
headers={"Authorization": f"Bearer {os.environ['OPENQR_KEY']}"},
timeout=10,
)
if resp.status_code != 200:
# error responses are JSON: {"error": "..."}
try:
detail = resp.json().get("error", resp.text)
except ValueError:
detail = resp.text
raise RuntimeError(f"OpenQR {resp.status_code}: {detail}")
return resp.content| Status | Meaning | Fix |
|---|---|---|
| 200 | Image rendered | Read resp.content (PNG) or resp.text (SVG). |
| 400 | Bad request | data is missing, over 2000 chars, or the JSON body is invalid. |
| 401 | Unauthorized | Missing or wrong key — send Authorization: Bearer oqr_… |
These are static codes
Everything above produces static QR codes: the data is encoded directly into the image, so the code never expires and needs no server to resolve. You cannot change where it points after printing. If you need an editable, trackable code, that is a dynamic code created via a different endpoint.
When you need to repoint a printed code or read scan analytics, reach for dynamic codes, which expose both over the same API from Python. For the wider API tour — every parameter, the OpenAPI spec, and other languages — see getting started with the QR code API. If you are generating codes in bulk, the bulk generation guide covers batching, and there is a typed TypeScript SDK if part of your stack is Node.
Get your free API keySign in with a magic link, create a key, and call /v1/qr from Python in minutes — free, no card.