Skip to content

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.

by Kyllian 7 min read

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.

ParameterTypeDefaultValues
datastringany payload
sizeinteger512642048
margininteger2016
ecenumml | m | q | h
darkcolourblack#rgb, #rrggbb, #rrggbbaa, black, white, transparent, purple
lightcolourwhiteas dark
formatenumpngpng | svg
downloadbooleanfalse1 | 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">
![QR code for example.com](https://makeqrco.de/qr/https://example.com)

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 withCauseFix
Invalid size: — and the same for margin, ec, dark, light, format, downloadA value is out of range or not understoodThe 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 longShorten 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?
The browser strips everything from the # onwards before the request is sent, so the fragment never reaches the server and the code encodes a truncated URL. Percent-encode it as %23, or pass the whole address through ?data= with encodeURIComponent applied. From curl or a server-side fetch the same rule applies: encode the hash yourself.
What is the largest QR code image the endpoint will return?
2048 pixels square for a PNG. Above that the request is rejected with a 400 rather than clamped. If you need a larger image, ask for format=svg instead: the vector output has no pixel dimensions to run out of and scales to a billboard from the same few hundred bytes.
Why does the API return 400 instead of quietly fixing my parameter?
Because a silently ignored typo is a bug you find in print. An out-of-range size, an unknown error correction letter or a colour that is not a hex value all fail loudly, and the plain-text response body names the offending parameter and lists its allowed values, so the fix is visible in the response itself.
Can I fetch the QR code endpoint from JavaScript in a browser?
Yes. Every response carries Access-Control-Allow-Origin: *, so a fetch from any origin works and you can turn the response into a blob or an object URL. A plain fetch sends no preflight; if you add a custom header the browser sends an OPTIONS request first, and the endpoint answers that with 204 and a 24-hour preflight cache.

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

← All guides