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/C500Step 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.zipStep 4: Unpack the ZIP
unzip batch.zip -d ./qrs/
ls qrs/
# manifest.json
# qr-0001.png
# qr-0002.png
# ...
# qr-0500.pngRead 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
| Limit | Value |
|---|---|
| Max items per call | 50 (Starter), 1 000 (Pro), 5 000 (Agency / Enterprise) |
| Max ZIP size | 100 MB |
| Logo support | logo_url only (no multipart in batch mode) |
| Quota cost | 1 credit per item |
| All-or-nothing | Yes |
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
- Bulk concept page: why all-or-nothing
- API reference: bulk endpoint
- Dynamic campaign with analytics