Skip to main content
ER ExaRoutes

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:

FieldRequired?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
}
FieldTypeMeaning
bulkIdstringSame id you submitted with.
statusenum"processing" | "completed" | "resultDownloaded"
countintegerNumber 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

EndpointStatusCause
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).
any401 / 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.