Developers
How to generate QR codes in PHP
By Sam Moreton · updated 30 June 2026
The usual advice for QR codes in PHP is to composer require endroid/qr-code and render locally. That is genuinely fine for many apps. 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 Python or Node, an HTTP call is the cleaner option. This guide shows how to generate QR codes in PHP by calling the free OpenQR REST API with cURL (and Guzzle as an alternative) — 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 endroid/qr-code and bacon/bacon-qr-code are good tools. If you are building a single app, 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 Composer package to pin or audit, and no reliance on the
gdorimagickextension being present and configured. If your PHP build has cURL (almost all do), you are done. - Consistent server-side output — the same endpoint returns the same image whether it runs in a web request, a queue worker, a cron job or a serverless function. 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 PHP 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 — endroid/qr-code is mature and well maintained. 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 load it from a .env file via your framework) and read it with getenv(). 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 script. A GET request with the data in the query string, the key read from the environment, and the response bytes written straight to disk. http_build_query() URL-encodes the params for you.
<?php
$key = getenv('OPENQR_KEY');
$query = http_build_query([
'data' => 'https://example.com',
'format' => 'png',
'size' => 600,
]);
$ch = curl_init("https://openqr.uk/v1/qr?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("OpenQR returned $status");
}
file_put_contents('qr.png', $body); // $body 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. Read CURLINFO_HTTP_CODE and bail on anything other than 200, so you never write an error page to qr.png. If you skip the check, file_put_contents() will happily save the JSON body as a broken image.
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.
<?php
$key = getenv('OPENQR_KEY');
$payload = json_encode([
'data' => 'WIFI:T:WPA;S:My Network;P:super-secret-password;;',
'format' => 'png',
'size' => 800,
'margin' => 2,
]);
$ch = curl_init('https://openqr.uk/v1/qr');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("OpenQR returned $status");
}
file_put_contents('wifi.png', $body);The same call with Guzzle
If your project already uses Guzzle (common in Laravel and Symfony apps), the call is tidier. Guzzle throws on non-2xx by default, so you get error handling for free, and you read the body off the response.
<?php
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://openqr.uk']);
$response = $client->get('/v1/qr', [
'query' => ['data' => 'https://example.com', 'format' => 'png', 'size' => 600],
'headers' => ['Authorization' => 'Bearer ' . getenv('OPENQR_KEY')],
'timeout' => 10,
]);
// Guzzle throws GuzzleHttp\Exception\RequestException on 4xx/5xx by default
$bytes = (string) $response->getBody();
file_put_contents('qr.png', $bytes);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.
<?php
$key = getenv('OPENQR_KEY');
$query = http_build_query([
'data' => 'https://example.com',
'format' => 'svg',
'size' => 480,
'dark' => '232E3A', // foreground hex, no '#'
'light' => 'FFFFFF', // background hex, no '#'
]);
$ch = curl_init("https://openqr.uk/v1/qr?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_TIMEOUT => 10,
]);
$svg = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("OpenQR returned $status");
}
file_put_contents('qr.svg', $svg); // $svg is the raw <svg> markupEmbedding in a web response
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 plain-PHP endpoint that proxies a PNG to the browser, fetching on demand and returning the bytes with the right content type.
<?php
// qr.php — stream a PNG back to the browser
$data = $_GET['data'] ?? '';
if ($data === '') {
http_response_code(400);
exit('missing "data"');
}
$query = http_build_query(['data' => $data, 'format' => 'png', 'size' => 512]);
$ch = curl_init("https://openqr.uk/v1/qr?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('OPENQR_KEY')],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
http_response_code(502);
exit('QR service error');
}
header('Content-Type: image/png');
echo $body;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 — on disk, in APCu, in Redis, 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 (Dompdf, mPDF, TCPDF) 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.
<?php
function qrPngDataUri(string $data): string {
$query = http_build_query(['data' => $data, 'format' => 'png', 'size' => 480]);
$ch = curl_init("https://openqr.uk/v1/qr?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('OPENQR_KEY')],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("OpenQR returned $status");
}
return 'data:image/png;base64,' . base64_encode($body);
}
// Use it in an email or PDF template:
$src = qrPngDataUri('https://example.com');
$imgTag = '<img src="' . $src . '" 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 treating the body as an image before checking the status. Branch on the HTTP code; on failure, decode the JSON for a useful message. The helper below centralises that logic.
<?php
function makeQr(string $data, string $format = 'png', int $size = 512): string {
$query = http_build_query(['data' => $data, 'format' => $format, 'size' => $size]);
$ch = curl_init("https://openqr.uk/v1/qr?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('OPENQR_KEY')],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('OpenQR request failed: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
// error responses are JSON: {"error": "..."}
$detail = json_decode($body, true)['error'] ?? $body;
throw new RuntimeException("OpenQR $status: $detail");
}
return $body;
}| Status | Meaning | Fix |
|---|---|---|
| 200 | Image rendered | The body is the raw PNG or SVG — echo or save it. |
| 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 PHP. 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 Python walkthrough too if part of your stack is Python.
Get your free API keySign in with a magic link, create a key, and call /v1/qr from PHP in minutes — free, no card.