POST /api/v1/generate/bulk/
Render up to 5 000 QRs in a single call and receive a ZIP archive plus a manifest.json describing every item.
POST /api/v1/generate/bulk/
Render up to plan.max_bulk_items QRs in a single call. Returns a ZIP
archive (Content-Type: application/zip) containing one image per item
plus a manifest.json.
Free plan is locked out (403). Starter caps 50, Pro 1 000, Agency and
Enterprise 5 000.
Endpoint
POST /api/v1/generate/bulk/
Host: api.qrstudio.agency
X-Api-Key: smk_...
Content-Type: application/jsonBody
{
"items": [
{ "data": "https://example.com/1", "size_inches": 3 },
{ "data": "https://example.com/2", "size_inches": 3 },
{
"data_type": "vcard",
"payload": { "name": "Alice", "phone": "+15145550001" },
"size_inches": 2
},
{
"data_type": "dynamic",
"payload": {
"name": "Promo card 003",
"destination_url": "https://example.com/promo/003"
},
"size_inches": 4
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
items | array of GenerateRequest | yes | Each item has the same schema as POST /generate/. Min 1, max plan.max_bulk_items. |
info: Logos in bulk Bulk only accepts
logo_urlandbg_image_url. Multipart in batch mode is intentionally unsupported. Host assets on a CDN.
Response
200 OK
HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="qrstudio-bulk-50.zip"
X-QR-Plan: starter
X-QR-Quota-Remaining: 437
X-Request-Id: 01HZ8X...
<binary ZIP bytes>The ZIP contains:
| File | Content |
|---|---|
qr-0001.png (or .svg) | First item's render |
qr-0002.png | Second item's render |
| ... | ... |
qr-NNNN.png | Nth item's render |
manifest.json | Index map (stored uncompressed for zero-dep readers) |
manifest.json shape
{
"count": 50,
"plan": "starter",
"items": [
{
"index": 0,
"filename": "qr-0001.png",
"data_type": "url",
"format": "png",
"size_inches": 3,
"dpi": 300,
"bytes": 18432,
"cache": "MISS",
"duration_ms": 312,
"has_logo": false
},
{
"index": 3,
"filename": "qr-0004.png",
"data_type": "dynamic",
"format": "png",
"size_inches": 4,
"dpi": 300,
"bytes": 22871,
"cache": "MISS",
"duration_ms": 287,
"has_logo": false,
"dynamic": {
"short_id": "aBc12dEf",
"public_url": "https://q.qrstudio.agency/q/aBc12dEf/"
}
}
]
}| Manifest field | Type | Description |
|---|---|---|
count | int | Number of rendered items |
plan | string | The plan code that rendered the batch |
items[].index | int | 0-based position in the original items[] request |
items[].filename | string | Filename inside the ZIP |
items[].data_type | string | url, vcard, dynamic, etc. |
items[].format | string | png or svg |
items[].bytes | int | Size of the image in bytes |
items[].cache | string | HIT or MISS |
items[].duration_ms | int | Render time, 0 on cache hit |
items[].has_logo | bool | Whether a logo was composited |
items[].dynamic.short_id | string | Present when data_type: "dynamic" |
items[].dynamic.public_url | string | Present when data_type: "dynamic" |
400 Bad Request
Plan does not include bulk, or items count exceeds plan cap, or output ZIP exceeded the 100 MB ceiling.
{
"detail": "Plan 'free' doesn't include bulk generation. Upgrade to Starter or higher."
}{
"items": "Plan 'starter' caps bulk at 50 items per request, got 200."
}422 Unprocessable Entity
Per-item validation failure. The whole batch is rejected; we never emit a partial ZIP.
{
"errors": {
"3": "size_inches > plan max (8)",
"17": "Invalid color 'rouge'. Use 'black', 'white', or hex like #RRGGBB."
},
"rendered_before_failure": 17
}rendered_before_failure indicates how many items had successfully
rendered before the first per-item failure stopped the batch. Your
quota is not consumed.
429 Too Many Requests
Bulk size would push you past your monthly quota and your plan does not allow overage.
{
"detail": "Bulk of 50 would exceed your monthly quota (480/500 used)."
}Plan caps reference
| Plan | max_bulk_items |
|---|---|
| Free | n/a (endpoint disabled) |
| Starter | 50 |
| Pro | 1 000 |
| Agency | 5 000 |
| Enterprise | 5 000 |
Hard limits
| Limit | Value |
|---|---|
| Max items per batch | 5 000 (or plan cap, whichever is lower) |
| Max ZIP size | 100 MB |
| Max single item pixels | 9 000 x 9 000 |
| Max client timeout we suggest | 180 s for 5 000 items |
Quota accounting
- One quota credit per item.
- Counter is bumped atomically once per batch (single UPDATE for the whole batch).
- Cache hits still count.
- Failed batches (
422,429) consume zero quota.
Examples
Python
import requests
items = [
{"data": f"https://yourbrand.com/c/{i:04d}", "size_inches": 3}
for i in range(50)
]
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={"items": items},
timeout=120,
)
if r.status_code == 422:
print("Errors:", r.json())
else:
r.raise_for_status()
open("batch.zip", "wb").write(r.content)Node.js
import fs from "node:fs";
const items = Array.from({ length: 50 }, (_, i) => ({
data: `https://yourbrand.com/c/${String(i).padStart(4, "0")}`,
size_inches: 3,
}));
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({ items }),
});
if (r.status === 422) {
console.error("Errors:", await r.json());
} else if (!r.ok) {
throw new Error(`HTTP ${r.status}`);
} else {
fs.writeFileSync("batch.zip", Buffer.from(await r.arrayBuffer()));
}