Bulk export to ZIP
Render thousands of distinct QRs in one HTTP call and unpack the ZIP archive returned by the API.
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.
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/C500Build the request body
Every entry in items[] is a complete GenerateRequest. Mix and match
freely.
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")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`);POST to the bulk endpoint
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")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 -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.zipUnpack 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']})")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.
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