Developers
Generate and manage QR codes at scale: bulk + folders
By OpenQR · updated 26 June 2026
Generating one QR code is trivial. Generating four hundred — each pointing somewhere different, each labelled, each filed so you can find it again — is an ops problem. This guide covers the OpenQR bulk endpoint, folders for organisation, and listing/exporting your codes, then walks through a real script that turns a CSV of destinations into a print-ready label-to-short-URL map.
9 min read · Updated 26 June 2026

Not writing code? The free bulk QR code generator makes a whole set in the browser from a pasted list, with no account and no key. This article is for doing it programmatically. If you have not set up an API key yet, start with getting started with the OpenQR API — you sign in with a magic link at openqr.uk/api, create a key, and send it as Authorization: Bearer oqr_… on every request. This article assumes you have a key and have read the dynamic codes guide, since bulk creation produces the same editable, trackable codes — just many at once.
Why bulk, and the limits that make it necessary
Single-code creation via POST /v1/dynamic is rate-limited to 300 codes per rolling hour, per account. That cap is deliberate abuse protection, and it is fine for occasional codes; minting a thousand codes one POST at a time is still a non-starter. The bulk endpoint is the volume path: POST /v1/dynamic/bulk creates up to 200 codes in a single request, and it is the correct tool whenever you need more than a handful.
Three limits bound a bulk run
Both creation paths draw on the same rolling window of 300 codes per hour per account, and a bulk request consumes one unit per code: a 200-code batch needs 200 units of budget, and a request that exceeds what remains is rejected up front with 429 (nothing is created). The bulk endpoint itself caps at 200 codes per request, so a 1,000-code run is five requests; keep them sequential rather than firing dozens in parallel. The plan's active-code cap applies in full too: a batch of N needs N free active slots, so large runs need Pro.
The bulk endpoint
Send a JSON body of { "codes": [{ "destination", "label"? }] } (a bare array also works). Each destination must be a public http(s) URL: private/internal hosts and oqr.to self-loops are rejected. label is optional: your own human-readable name for the code. A 201 returns the created codes, each with its id, slug and short_url (the https://oqr.to/<slug> the QR will encode).
You can also send a top-level theme — the id or name of a saved theme (its colours, dot and corner shapes, logo, gradient and frame) — and every code in the batch is created with that style. It applies to the whole request, not per code, so a branded run comes out visually consistent in a single call. Save themes in the dashboard or via POST /v1/themes, and list them with GET /v1/themes.
curl -s https://openqr.uk/v1/dynamic/bulk \
-H "Authorization: Bearer oqr_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"codes": [
{ "destination": "https://acme.com/listing/12-oak-avenue", "label": "12 Oak Avenue" },
{ "destination": "https://acme.com/listing/4-elm-court", "label": "4 Elm Court" }
],
"theme": "Brand"
}'{
"created": [
{
"id": "9b1c…",
"slug": "k7Hq2mP",
"short_url": "https://oqr.to/k7Hq2mP",
"destination": "https://acme.com/listing/12-oak-avenue",
"label": "12 Oak Avenue"
},
{
"id": "3f8a…",
"slug": "Tn4xR9d",
"short_url": "https://oqr.to/Tn4xR9d",
"destination": "https://acme.com/listing/4-elm-court",
"label": "4 Elm Court"
}
]
}Validation is all-or-nothing per request
If any row has an invalid destination, the whole request is rejected with a 400 that names the offending row (e.g. "Row 7: …"). Nothing is created. Clean your CSV first, or validate URLs client-side before you POST.
Folders: organising what you create
Once you are creating codes by the hundred, you need somewhere to put them. Folders are a flat, named grouping under /v1/folders:
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /v1/folders | — | { folders: [{ id, name }] } |
| POST | /v1/folders | { name } | 201 { id, name } |
| PATCH | /v1/folders/{id} | { name } | { id, name } |
| DELETE | /v1/folders/{id} | — | { deleted: true } |
The bulk endpoint itself does not take a folder_id — it only accepts destination and label. You file codes after creating them, by patching each code: PATCH /v1/dynamic/{id} with { "folder_id": "<folder-id>" }. Pass null to un-file a code. The same PATCH is where you can also set a custom slug, tags, or repoint the destination later.
Deleting a folder doesn't delete its codes
DELETE /v1/folders/{id} removes the folder and un-files its codes (sets their folder_id to null) — the codes themselves, and every printed QR pointing at them, keep working. There is no way to accidentally nuke a print run by tidying up folders.
Listing and exporting your codes
GET /v1/dynamic?limit= returns your codes newest-first (limit up to 500, default 200). Each row carries id, slug, short_url, destination, label, status and created_at — enough to reconcile what you created, re-export a label-to-URL map, or feed a downstream system. For per-code scan analytics use GET /v1/dynamic/{id}/scans, covered in tracking QR code scans.
Worked example: a CSV in, a print map out
Here is the whole job end-to-end — it reads a CSV of destination,label, creates one folder for the batch, chunks the rows into requests of ≤200, bulk-creates them, files each into the folder, and writes print-map.csv mapping every label to its short_url — the file your print/design step needs. The Node version leans on the standard library only; the Python core loop uses requests — see our Python guide for more. If you'd rather not run a script at all, you can drive the same one-code-per-row job from a spreadsheet: see QR codes in Google Sheets.
import { readFileSync, writeFileSync } from "node:fs";
const API = "https://openqr.uk";
const KEY = process.env.OPENQR_KEY; // "oqr_…"
const BULK_MAX = 200;
const headers = {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
};
// Minimal CSV reader: expects a "destination,label" header row.
function readCsv(path) {
const [head, ...lines] = readFileSync(path, "utf8").trim().split(/\r?\n/);
const cols = head.split(",").map((c) => c.trim());
return lines.map((line) => {
const cells = line.split(",");
return Object.fromEntries(cols.map((c, i) => [c, (cells[i] ?? "").trim()]));
});
}
function chunk(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
async function api(path, init) {
const res = await fetch(API + path, { ...init, headers });
if (!res.ok) throw new Error(`${path} → ${res.status}: ${await res.text()}`);
return res.json();
}
async function main() {
const rows = readCsv(process.argv[2] ?? "destinations.csv");
if (!rows.length) throw new Error("No rows in CSV.");
// 1. One folder for this print run.
const folder = await api("/v1/folders", {
method: "POST",
body: JSON.stringify({ name: `Print run ${new Date().toISOString().slice(0, 10)}` }),
});
// 2. Bulk-create in batches of <=200, collecting every created code.
const created = [];
for (const batch of chunk(rows, BULK_MAX)) {
const codes = batch.map((r) => ({ destination: r.destination, label: r.label }));
const { created: made } = await api("/v1/dynamic/bulk", {
method: "POST",
body: JSON.stringify({ codes }),
});
created.push(...made);
}
// 3. File each code into the folder (bulk can't set folder_id directly).
for (const c of created) {
await api(`/v1/dynamic/${c.id}`, {
method: "PATCH",
body: JSON.stringify({ folder_id: folder.id }),
});
}
// 4. Write the print map: label -> short_url.
const csv = ["label,short_url", ...created.map((c) => `${c.label ?? ""},${c.short_url}`)].join("\n");
writeFileSync("print-map.csv", csv);
console.log(`Created ${created.length} codes in folder "${folder.name}". Wrote print-map.csv.`);
}
main().catch((e) => { console.error(e); process.exit(1); });Where this pays off
Anywhere you need a unique, editable code per physical item — one short URL per object, all repointable later without reprinting:
- Real-estate signage — one code per listing on every board and flyer; repoint it to the next property when the first sells.
- Asset and inventory tags — a code per machine, room or shelf that opens its maintenance log or manual.
- Retail SKUs — per-product codes that route to the live product page, swappable when a line is discontinued.
- Conference badges — a unique code per attendee linking to their profile, schedule or check-in.
- Per-item campaigns — uniquely coded packaging or coupons so each scan is attributable.
Because these are dynamic codes, every one is editable and trackable after printing — you can repoint any destination through the dashboard, the API, or the MCP server if you would rather drive it from an AI agent (the bulk_create_dynamic_qr, create_folder and update_dynamic_qr tools mirror these endpoints exactly, and bulk_create_dynamic_qr accepts the same optional theme argument to brand a whole batch).
Rendering the actual QR images
The bulk endpoint returns short_urls, not images. To print, render each short_url as a QR with GET /v1/qr?data=