Bulk generate
Submit an array of QR rows in a single request. The API queues the batch
for asynchronous worker processing and returns a bulkId
immediately. Use the dashboard's Bulk
page to download the result archive when the order reports
completed.
Endpoint
POST https://api.exaroutes.com/api/qr/bulk-generate Both auth headers required. Body: a JSON array. Max body: 25 MB.
Request body
The body is a JSON array. Each element is a QR row using the same shape as single QR generation with one extra requirement:
| Field | Required? | Notes |
|---|---|---|
uid | required on every row | Caller-supplied unique id. Echoed back in the result archive so you can match returned QRs to your input. Rows missing uid are recorded as { status: "failed", code: 400 } and do not generate a QR. |
| everything else | optional | See Generate QR. qrType, dataType, data, styling, secure, sources, expiry, … |
Caps
- API-key path (
POST /api/qr/bulk-generate): no fixed row cap. Bounded by the 25 MB JSON limit and your plan's QR-creation quota. Split very large batches across multiple calls. - Workspace bulk-generate (in-app CSV upload at
/api/qr/workspace-bulk-generate): max 500 rows per call. Rate-limited to 5 calls / minute / IP.
Example
curl https://api.exaroutes.com/api/qr/bulk-generate \
-H "client-id: cust_abc123…" \
-H "api-key: sk_live_xyz789…" \
-H "Content-Type: application/json" \
-d '[
{ "uid": "row-001", "qrType": "dynamic", "dataType": "url", "data": "https://example.com/lp/a" },
{ "uid": "row-002", "qrType": "dynamic", "dataType": "url", "data": "https://example.com/lp/b" },
{ "uid": "row-003", "qrType": "dynamic", "dataType": "url", "data": "https://example.com/lp/c" }
]'
Immediate response
HTTP/1.1 200 OK
Content-Type: application/json
"bulk_a8f31de2c904"
The response body is a JSON-encoded string (the bulkId). Save
it. You'll need it to poll status and
download the result.
Check status
Bulk processing runs in a worker thread and is asynchronous. Poll the
status endpoint until status reads completed:
GET https://api.exaroutes.com/api/qr/bulk-generate/{bulkId}
Both auth headers required. Scopes to the calling subscription — an API key for one subscription cannot read another subscription's bulk state, even with a guessed id.
Example
curl https://api.exaroutes.com/api/qr/bulk-generate/bulk_a8f31de2c904 \
-H "client-id: cust_abc123…" \
-H "api-key: sk_live_xyz789…"
Response
{
"bulkId": "bulk_a8f31de2c904",
"status": "completed", // see below
"count": 3, // rows submitted
"failedFeatures": { "dynamicQr": 0, "securedQr": 0, "sourceQr": 0 },
"product": "qr",
"createdAt": 1781827200000, // epoch ms
"updatedAt": 1781827245123
}
Field Type Meaning bulkIdstring Same id you submitted with. statusenum "processing" | "completed" | "resultDownloaded" countinteger Number of rows submitted. failedFeaturesobject { "dynamicQr": n, "securedQr": n, "sourceQr": n } — count of failed rows by feature class. Populated when status === "completed". productstring "qr" createdAtinteger (epoch ms) When the batch was queued. updatedAtinteger (epoch ms) Last status transition.
processing — worker is running. Poll again in a few seconds. completed — archive is ready for one-time download via the result endpoint below. resultDownloaded — you've already pulled the archive (it's been deleted from S3). Subsequent result-fetches return 410 Gone.
Polling cadence: bulks of a few hundred rows typically complete in under 30 seconds. Start at 2-second intervals and back off (e.g. 2s → 5s → 15s → 30s) — the row-count is bounded by your batch size, not by overall load.
The dashboard's Bulk page reads from the same row, so a row created via API is visible there.
Download the result
Once status === "completed", fetch the result archive:
GET https://api.exaroutes.com/api/qr/bulk-generate/{bulkId}/result
Both auth headers required. The
download is one-shot: the response succeeds once, then
the row flips to resultDownloaded and the S3 object is
deleted. Save the body locally on first call.
Example
curl https://api.exaroutes.com/api/qr/bulk-generate/bulk_a8f31de2c904/result \
-H "client-id: cust_abc123…" \
-H "api-key: sk_live_xyz789…" \
-o bulk-result.json
Response — result archive
JSON-encoded string body — the result archive is the contained string. Decode and parse to get an array with one entry per submitted row, in submission order.
[
// Successful dynamic-QR row
{
"id": "qr_abc",
"uid": "row-001",
"url": "https://qr.exaroutes.com/r/h4k2x9",
"QRs": "data:image/png;base64,…"
},
// Successful static row — string body, no DB row
"data:image/png;base64,…",
// Failed row
{
"uid": "row-003",
"status": "failed",
"message": "Invalid URL provided for dataType: url",
"code": 400
}
]
The download is one-shot. The file is deleted from object storage as soon
as you read it. Save it locally on first download.
Per-row failures
Individual row failures don't fail the whole batch. They're written into
the result array in-place as { uid, status: "failed", message, code }
and tallied into failedFeatures on the BulkOrders
row so you can audit at a glance:
dynamicQr: row had qrType: "dynamic" and failed. securedQr: row had a non-empty secure array and failed. sourceQr: row had a non-empty sources array and failed.
(A single row can be tallied in multiple buckets if it had multiple feature flags.)
Errors
Endpoint Status Cause POST /bulk-generate400Body isn't an array, or array is empty. POST /bulk-generate413Body exceeds the 25 MB JSON limit. Split into smaller batches. POST /bulk-generate429Plan QR-creation limit reached for the rows already processed. Remaining rows are recorded as failed. POST /bulk-generate500Worker failed to spawn. Retry. If it persists, contact support. GET /bulk-generate/:bulkId400bulkId path param missing. GET /bulk-generate/:bulkId404No bulk order with that id under the calling subscription. GET /bulk-generate/:bulkId/result404No bulk order with that id under the calling subscription. GET /bulk-generate/:bulkId/result410Result archive is no longer available — already downloaded (status flipped to resultDownloaded) or never produced (worker hasn't completed yet — check status first). any 401 / 403Auth. See Authentication.
Note: per-row errors (invalid dataType,
missing data, etc.) do not bubble up to the HTTP
status. They land in the result archive. The POST itself
returns 200 as long as the batch was queued.