Developers
Generate QR codes from a URL: the image API, end to end
A working guide to the /qr/ image endpoint: both payload forms, every parameter and its real range, PNG versus SVG, caching, CORS and what each 400 means.
You need a QR code inside something you are generating — an invoice PDF, a ticket email, a dashboard tile — and you do not want to add an encoding library, a build step and a place to store the resulting files just to get one.
One GET, no key
GET https://makeqrco.de/qr/https://example.com
200 OK · Content-Type: image/png · 512×512
Everything after /qr/ is the payload. The server URL-decodes it, encodes it as
a QR symbol and streams back the image. With no options that is a 512-pixel
black-on-white PNG at error correction M with a two-module quiet zone. There is
no shortener and no redirect in the middle, so the symbol contains your URL
literally and a code you generate today keeps working whether or not this
endpoint does.
Two things trip people up on the first request. The // in https:// is
collapsed by a lot of HTTP stacks, so the endpoint restores https:/ back to
https:// for you — you can send either. And /qr/<url> is the image;
makeqrco.de/<url> without the /qr/ prefix is an HTML share page with Open
Graph tags, which will show as a broken image if you point an <img> at it.
Two ways to give it a payload
The path form is the one to reach for when the payload is a plain URL, including one with its own query string:
/qr/https://shop.example/p?id=42&ref=poster
The ?data= form is the one to reach for when it is anything else. It replaces
the path entirely, nothing else in the query string leaks into the payload, and
the render options still apply:
/qr/?data=WIFI%3AT%3AWPA%3BS%3ACafeGuest%3BP%3Ahunter2hunter2%3B%3B
/qr/?data=tel%3A%2B442079460000
/qr/?data=SMSTO%3A%2B447700900000%3ASee%20you%20at%20six
/qr/?data=geo%3A52.520008%2C13.404954
Run the payload through encodeURIComponent and you never have to think about
which characters matter. That is the only sane way to send a vCard, whose
\r\n line breaks would otherwise be mangled. The
WIFI: string reference has the exact syntax for the
first of those, and /wifi-qr-code/ will assemble one for you.
Every parameter
All eight are optional, all are validated, and out-of-range values are rejected rather than clamped.
| Parameter | Type | Default | Values |
|---|---|---|---|
data | string | — | any payload |
size | integer | 512 | 64–2048 |
margin | integer | 2 | 0–16 |
ec | enum | m | l | m | q | h |
dark | colour | black | #rgb, #rrggbb, #rrggbbaa, black, white, transparent, purple |
light | colour | white | as dark |
format | enum | png | png | svg |
download | boolean | false | 1 | true | yes | 0 | false | no |
A few of those need a sentence each.
size is the pixel width and height of the PNG. For SVG it stops mattering: the
file carries a viewBox and scales to whatever box your layout gives it.
margin is the quiet zone measured in modules, not pixels. The specification
asks for four; the default of two buys a tighter image. Ask for 1, 2 or 3
explicitly and you still get a 200, but the response carries an X-QR-Warning
header saying so. Use margin=4 for anything printed, and margin=0 only when
you are placing the code on your own padded background.
ec is case-insensitive, so ec=H and ec=h are the same request. The four
levels and what each one costs you in module count are in the
error correction guide.
dark and light take a leading # or not, and # is awkward in a URL, so
dark=7c3aed is the usual spelling. purple is the site’s own #7c3aed. The
eight-digit #rrggbbaa form gives you alpha.
download adds Content-Disposition: attachment, so the browser saves
qrcode.png or qrcode.svg instead of displaying it. A bare ?download with
no value counts as true.
There is no logo parameter. Logos are composited onto a canvas in the browser by the generator, so a logo code has to come from there rather than from a URL.
PNG or SVG
/qr/https://example.com?size=1024 → PNG, 1024×1024
/qr/https://example.com?format=svg → SVG, a few hundred bytes
PNG when something has to treat the result as a bitmap: an <img> in an HTML
email, a canvas, a slide, an OG image, anything that will be resized down but
never up. SVG for print, for large format, and for the web when you care about
the transfer size — the vector file is usually smaller than a 256-pixel PNG of
the same code and sharp at any size. Which to use when is the whole of
the file formats guide.
Transparent backgrounds
/qr/https://example.com?dark=7c3aed&light=transparent
light=transparent gives a genuinely transparent PNG, and an SVG with no
background rectangle at all rather than a white one. That is what you want when
the code sits on a coloured panel — but the modules still need to be much darker
than whatever shows through, and a photograph behind a transparent code is a
reliable way to make it unscannable.
If dark and light end up the same colour, you get the image and an
X-QR-Warning header telling you it has no contrast.
When your URL has its own query string
The payload lives in the path, and the query string is glued onto it. That is
deliberate — it is why a tracking URL works with no escaping at all. Only the
eight names in the table are claimed as options, matched exactly and
case-sensitively, so ?Size=1024 with a capital S is payload, and ?ref=poster
is payload.
The honest consequence is one collision. If your target URL carries size,
margin, ec, dark, light, format, download or data of its own, the
request is genuinely ambiguous:
/qr/https://shop.example/p?size=XL → 400: size=XL is not a whole number
Two escape hatches, both exact:
/qr/https%3A%2F%2Fshop.example%2Fp%3Fsize%3DXL
/qr/?data=https%3A%2F%2Fshop.example%2Fp%3Fsize%3DXL
From a script, use the second one every time and the problem never arises.
Caching and CORS
Successful responses carry
Cache-Control: public, max-age=86400, s-maxage=86400, immutable and
Access-Control-Allow-Origin: *. The cache key is the entire URL, options
included, so two option sets are two cached images and repeating a link is
effectively free. SVG responses add Vary: Accept-Encoding, because the
compressed and uncompressed bytes differ under the same URL.
Everything is X-Robots-Tag: noindex, nofollow, which matters if you are
embedding codes on a public page: the image will not compete with your own
content in search results.
Embedding it
<img src="https://makeqrco.de/qr/https://example.com?size=256&dark=7c3aed"
alt="QR code for example.com" width="256" height="256" loading="lazy">

HTML email. PNG only, absolute URL, explicit width and height, real
alt text. Most clients block remote images until the reader allows them, and
several fetch through an image proxy — both are fine here, because the response
is public and cacheable, but it does mean the code must be worth waiting for.
Notion. Paste the /qr/ URL into an image block and choose the embed-by-link
option; Notion fetches it like any other remote image.
Shell.
curl -o qr.svg "https://makeqrco.de/qr/https://example.com?format=svg&download=1"
React, with the payload encoded rather than pasted into the path:
export function QrCode({ value, size = 256, ec = 'm' }) {
const src =
`https://makeqrco.de/qr/?data=${encodeURIComponent(value)}` +
`&size=${size}&ec=${ec}&margin=4`;
return (
<img src={src} width={size} height={size} loading="lazy" decoding="async"
alt={`QR code linking to ${value}`} />
);
}
What the 400s mean
There are only three shapes of failure, and all of them return text/plain you
can read directly.
| Body starts with | Cause | Fix |
|---|---|---|
Invalid size: — and the same for margin, ec, dark, light, format, download | A value is out of range or not understood | The message names the parameter and lists its allowed values |
No URL provided. | Empty path and no ?data= | Put a payload after /qr/, or use ?data= |
Failed to generate QR code: | The payload cannot be encoded — almost always too long | Shorten it; the ceiling is roughly 2,300 characters, and far lower at ec=h |
That last one is the one to design against. A QR code holds a fixed number of modules per version, and a long payload pushes the version up until each module is too small to print. Encode a short link and put the long thing behind it — the reasoning, in millimetres, is in what size a printed QR code should be.
The full reference table, with live examples rendered by the endpoint itself, lives on /qr-code-api/, which renders it from the same constants the parser enforces — so it cannot drift from what the endpoint does.
Frequently asked
What happens if the URL I want to encode contains a # fragment?
What is the largest QR code image the endpoint will return?
Why does the API return 400 instead of quietly fixing my parameter?
Can I fetch the QR code endpoint from JavaScript in a browser?
- api
- developers
- svg
- embedding
- png
Make the code this guide describes
The generator runs in your browser — add a logo, pick colours, tag the link with UTM parameters and download a PNG or SVG. No account, no watermark.
Keep reading
-
Printing
PNG, SVG or PDF: which QR code file do you actually need
Why a scaled-up PNG scans worse, the pixel size a given print size really needs at 300 dpi, and how to get a QR code into InDesign, Word, Canva or a PDF.
-
Printing
Test a QR code properly before you print a thousand of them
A repeatable procedure for proving a QR code before the print run: final stock, three phones, lighting, angle sweeps, the destination and ISO grading.