QR code reader API
POST /v2/convert/qrcode/to/data looks for QR codes only, in an image or on each page of a PDF, and returns {"barcodes": [...]} with the text and page of each. The two-page ticket PDF in the sample gave 3 codes with DetectMultiple=true, and skipped the Code 128 printed next to the first ticket.
- Endpoint
- POST /v2/
convert/ qrcode/ to/ data - Accepts
- qrcode
- Sample timing
- 25 ms median of 5 runsHow we measure
- Request size
- Up to 10 MB on the Free plan or 100 MB on paid plans, all files together
Request and response
Every sample below ran against the API before this page was published. Set PDFCONVERSIONAPI_KEY to your key and run it as is.
curl -sS -X POST \
https://api.pdfconversionapi.com/v2/convert/qrcode/to/data \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@swap-tickets.pdf \
--form-string DetectMultiple=true \
-o response.json
# FileData is base64. Save the first file in converted/,
# or print the JSON error.
if jq -e '.Files' response.json > /dev/null; then
mkdir -p converted
name=$(jq -r '.Files[0].FileName' response.json)
jq -r '.Files[0].FileData' response.json \
| base64 --decode > "converted/$name"
else
cat response.json
fiimport base64
import os
import requests
key = os.environ["PDFCONVERSIONAPI_KEY"]
response = requests.post(
"https://api.pdfconversionapi.com/v2/convert/qrcode/to/data",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("swap-tickets.pdf", "rb"))],
data={
"DetectMultiple": "true",
},
timeout=180,
)
result = response.json()
if not response.ok:
raise SystemExit(f"{result['Code']}: {result['Message']}")
os.makedirs("converted", exist_ok=True)
for file in result["Files"]:
path = os.path.join("converted", file["FileName"])
with open(path, "wb") as out:
out.write(base64.b64decode(file["FileData"]))
print(path, file["FileSize"], "bytes")
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const form = new FormData();
const file = "swap-tickets.pdf";
form.append("File", new Blob([await readFile(file)]), file);
form.append("DetectMultiple", "true");
const url = "https://api.pdfconversionapi.com/v2/convert/qrcode/to/data";
const key = process.env.PDFCONVERSIONAPI_KEY;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
});
const result = await response.json();
if (!response.ok) {
throw new Error(`${result.Code}: ${result.Message}`);
}
await mkdir("converted", { recursive: true });
for (const file of result.Files) {
const path = join("converted", file.FileName);
await writeFile(path, Buffer.from(file.FileData, "base64"));
console.log(path, file.FileSize, "bytes");
}
{
"ConversionCost": 1,
"ConversionTime": 0.025,
"Files": [
{
"FileName": "swap-tickets.json",
"FileExt": "json",
"FileSize": 377,
"FileData": "ewogICJiYXJjb2RlcyI6IFsK…"
}
]
}Response from our test run on 2026-11-03. ConversionTime shows the median of 5 runs. FileData is shortened here; the real response carries the whole file as base64.
The sample, converted
The same file the code sends. Download the input and the output and compare them yourself.
swap-tickets.json, 19 lines
{ "barcodes": [ { "format": "qrcode", "page": 1, "text": "https://tickets.example/e/seed-swap-2027/A-0142" }, { "format": "qrcode", "page": 2, "text": "WIFI:T:WPA;S:SeedSwapGuest;P:compost2027;;" }, { "format": "qrcode", "page": 2, "text": "https://tickets.example/e/seed-swap-2027/A-0143" } ]}
- Input
- swap-tickets.pdf (39 KB)
- Output
- swap-tickets.json (377 bytes)
- Source
- Two pages of drawn entry tickets for a fictional seed swap, made for this site: a ticket QR code on each page, a guest Wi-Fi QR code on page 2 and a Code 128 bar code on page 1. (CC0-1.0). How it was made
- Made by
- 2026-11-04, API build
9eeeb0640aba
Parameters
Send these as form fields next to the file. Names are case-insensitive. If you set one of the documented options below that we don't support yet, the request fails with a 400 that names it instead of returning a file made without it.
| Parameter | What it does |
|---|---|
DetectMultipleboolean, default false | Off, the reader stops at the first page with a QR code and returns one: the sample's page-1 ticket. On, it returns all three, page 2's Wi-Fi code and second ticket included. |
PageRangestring, default 1-2000 | Which PDF pages to search. 2 and 2- both gave only page 2's two codes; 3 returned 400 "PageRange asks for page 3, but the document has 2 pages".Values: e.g. 2, 1-3, 2- |
IncludeInvertedboolean, default false | Also looks for light-on-dark QR codes, which are skipped otherwise. |
EnhancedDetectionboolean, default false | Accepted for compatibility; the thorough search always runs, so the result doesn't change. |
AutoRotateboolean, default false | Accepted and has no effect: all orientations are tried. |
FileNamestring | Base name of the JSON result. FileName=ticket-codes gave ticket-codes.json instead of swap-tickets.json. |
StoreFileboolean, default false | true returns Files[].Url, kept for 3 hours, in place of the base64 JSON. |
What carries over, and what doesn't
Kept
- Each code's full text, byte for byte: the ticket URLs and the Wi-Fi string with all its semicolons
- The page of each code:
"page": 1for the first ticket,"page": 2for the other two - Several codes on one page: page 2 holds two, and both came back
- Text in ISO-8859-1 as well as UTF-8: a code holding the Latin-1 bytes for "Café" read as
Café
Lost or not yet supported
- Every non-QR code: the Code 128 on page 1 is not reported, and
BarcodeFormathas no effect here. - Where each code is, how big it is and its version and error correction level.
- Codes in a PDF with an open password: 422 "the input file is password-protected", even with the right
Password. - Any meaning inside the text. Wi-Fi, contact and calendar codes are returned as their raw strings.
Limits
- One image or PDF per request.
- PDF pages are drawn at 200 DPI before reading;
ImageResolutiondoesn't change that. Up to 2,000 pages per request. - Images up to 100,000,000 pixels.
- No QR code found returns 200 with
{"barcodes": []}, not an error. - Requests up to 10 MB on the Free plan or 100 MB on paid plans, all files and fields together; 120 seconds per request, then 504.
Where teams use it
Checking in tickets from PDFs
Event and travel tickets are often issued as multi-page PDFs with one QR code per ticket. Read every page in one request and match each ticket URL or ID against your list, page by page.
Pulling links and Wi-Fi details from flyers
A scanned flyer or menu may carry a QR code for a web page or a guest network. The text comes back as encoded, such as WIFI:T:WPA;S:SeedSwapGuest;P:compost2027;;, ready for your own parsing.
QR codes among other barcodes
Shipping labels and forms often carry a QR code next to linear barcodes. This route ignores the others, so the result lists only the QR content, without filtering by format.
Questions
How is this different from the PNG to QR data route?
/v2/convert/png/to/qrdata and its JPG, GIF, BMP, WebP and TIFF versions take one image and return {"codes": [...]}, with 422 when there is no code. This route also takes PDFs, reports the page of each code, follows DetectMultiple, PageRange and IncludeInverted, and returns an empty list when nothing is found.
Why did I get one code when my PDF has several?
Without DetectMultiple=true the search stops at the first page that has a QR code and returns one result, as ConvertAPI's default does. The sample then gives only the page-1 ticket.
Will small QR codes in a PDF be read?
Pages are drawn at 200 DPI, so a code's modules need to come out a few pixels wide at that resolution. The sample's ticket codes, 164 px at 120 DPI in the PDF (about 273 px when drawn), read without trouble.
Can I read QR codes from a password-protected PDF?
Not yet: the Password field is accepted but the PDF isn't opened with it. Unprotect it first with /v2/convert/pdf/to/unprotect, then send the result here.
Convert your first QRCODE file in a minute.