QRQR Code Agency
Guides

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/C500

Build 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.zip

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']})")

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

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