QRQR Code Agency
Guides

Tutorial - Bulk export to ZIP

Render thousands of distinct QRs in one HTTP call and unpack the ZIP archive returned by the API.

Tutorial: Bulk export to ZIP

You have a CSV with 500 customer URLs. You want one PNG per row, named qr-0001.png through qr-0500.png, ready to send to the sticker printer. This guide walks through the bulk endpoint end to end.

What you will build

A folder of 500 PNGs plus a manifest.json that maps each filename to its source row.

Requires Starter plan or higher.

Step 1: Read your input

Suppose customers.csv looks like:

id,landing_page
C001,https://yourbrand.com/c/C001
C002,https://yourbrand.com/c/C002
...
C500,https://yourbrand.com/c/C500

Step 2: Build the request body

Every entry in items[] is a complete GenerateRequest. Mix and match freely.

Python

import csv
import json

with open("customers.csv") as f:
rows = list(csv.DictReader(f))

body = {
"items": [
{
"data": row["landing_page"],
"size_inches": 3,
"color": "black",
"background": "white",
"pattern": "rounded",
}
for row in rows
],
}
print(f"Prepared {len(body['items'])} items")

Node.js

import { readFileSync } from "node:fs";
import { parse } from "csv-parse/sync";

const rows = parse(readFileSync("customers.csv"), { columns: true });

const body = {
items: rows.map(row => ({
data: row.landing_page,
size_inches: 3,
color: "black",
background: "white",
pattern: "rounded",
})),
};
console.log(`Prepared ${body.items.length} items`);

Step 3: POST to the bulk endpoint

Python

import requests

r = requests.post(
"https://api.qrstudio.agency/api/v1/generate/bulk/",
headers={
"X-Api-Key": os.environ["QRSTUDIO_API_KEY"],
"Content-Type": "application/json",
},
json=body,
timeout=120, # bulk batches can take a while
)
if r.status_code == 422:
print("Validation errors:", r.json())
sys.exit(1)
r.raise_for_status()

with open("batch.zip", "wb") as f:
f.write(r.content)
print(f"Saved {len(r.content)} bytes -> batch.zip")

Node.js

const r = await fetch("https://api.qrstudio.agency/api/v1/generate/bulk/", {
method: "POST",
headers: {
"X-Api-Key": process.env.QRSTUDIO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

if (r.status === 422) {
console.error("Validation errors:", await r.json());
process.exit(1);
}
if (!r.ok) throw new Error(`HTTP ${r.status}`);

fs.writeFileSync("batch.zip", Buffer.from(await r.arrayBuffer()));
console.log("Saved batch.zip");

cURL

curl -X POST https://api.qrstudio.agency/api/v1/generate/bulk/ \
-H "X-Api-Key: smk_..." \
-H "Content-Type: application/json" \
-d @body.json \
--output batch.zip

Step 4: Unpack the ZIP

unzip batch.zip -d ./qrs/
ls qrs/
# manifest.json
# qr-0001.png
# qr-0002.png
# ...
# qr-0500.png

Read manifest.json to map filenames back to your source rows:

import json
with open("qrs/manifest.json") as f:
 manifest = json.load(f)

for item in manifest["items"]:
 src_row = rows[item["index"]]
 print(f"{src_row['id']} -> qrs/{item['filename']} ({item['bytes']} bytes, {item['cache']})")

Step 5: Handle errors

The bulk endpoint is all-or-nothing. If any item fails validation, the whole batch fails with 422:

{
 "errors": {
 "3": "size_inches > plan max (8)",
 "17": "Invalid color 'rouge'. Use 'black', 'white', or hex like #RRGGBB."
 },
 "rendered_before_failure": 17
}

Fix every offending item and re-submit. We never emit a partial ZIP. Your quota is not consumed if the batch is rejected.

Step 6: Mix dynamic items in the same batch

The same batch can include static URL items and dynamic items. Each dynamic item creates a row in our database; the manifest captures the short id:

{
 "items": [
 {
 "data": "https://yourbrand.com/c/C001",
 "size_inches": 3
 },
 {
 "data_type": "dynamic",
 "payload": {
 "name": "Promo card C002",
 "destination_url": "https://yourbrand.com/c/C002"
 },
 "size_inches": 3
 }
 ]
}

Manifest excerpt for the dynamic item:

{
 "index": 1,
 "filename": "qr-0002.png",
 "data_type": "dynamic",
 "format": "png",
 "size_inches": 3,
 "bytes": 19847,
 "cache": "MISS",
 "duration_ms": 287,
 "has_logo": false,
 "dynamic": {
 "short_id": "aBc12dEf",
 "public_url": "https://q.qrstudio.agency/q/aBc12dEf/"
 }
}

Persist short_id for each dynamic item if you plan to PATCH or pull analytics later.

Bulk caps and limits

LimitValue
Max items per call50 (Starter), 1 000 (Pro), 5 000 (Agency / Enterprise)
Max ZIP size100 MB
Logo supportlogo_url only (no multipart in batch mode)
Quota cost1 credit per item
All-or-nothingYes

Performance tips

  • The render cache is shared with /generate/. Re-running the same bulk costs zero render time after the first call.
  • Duplicate items inside a batch are deduped by the cache; render once, emit N copies.
  • 5 000 high-DPI items can take 90-180 seconds. Bump your client timeout.

What is next

On this page