API documentation

Public API v1 · OpenAPI spec

The bulkqr API generates QR codes in bulk: create a job with up to a million items, poll its status, and download a ZIP of the results. Everything the generator page can do with images, the API can do headlessly.

Authentication

Requests are authenticated with an organisation API key sent as a bearer token. Owners and admins create keys on the account page; a key is shown once at creation and can be revoked at any time. Keys grant full job access for the organisation — store them like passwords, and roll them by creating a replacement key, switching your integration over, then revoking the old one (both keys work during the overlap).

Authorization: Bearer bqk_your_key_here

Quickstart

Create a job:

curl -X POST https://bulkqr.com/api/v1/jobs \
  -H "Authorization: Bearer $BULKQR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"data": "https://example.com/a"}, {"data": "https://example.com/b"}]}'

The response points at signed status and download URLs (valid 90 days, no auth needed):

{
  "jobId": "…",
  "total": 2,
  "statusUrl": "/d/…/status?exp=…&t=…",
  "downloadUrl": "/d/…?exp=…&t=…"
}

Poll statusUrl (or GET /api/v1/jobs/{jobId} with your key) until "status": "complete", then fetch downloadUrl (or GET /api/v1/jobs/{jobId}/download) for the ZIP.

Create a job — POST /api/v1/jobs

JSON bodies carry up to 20,000 items:

FieldTypeNotes
itemsarray, required1–20,000 of {"data", "filename"?, "caption"?}; data ≤ 2,953 bytes — see Styling for per-item captions and filenames
formatstringpng (default), svg, or both
styleobjectSee Styling — colours, transparency, logo, caption
sendEmailbooleanEmail the download link to the organisation owner. Default false for API jobs

Streaming (NDJSON) for large jobs

Up to 1,000,000 items / 100 MB with Content-Type: application/x-ndjson: the first line is the job meta, each following line one item.

{"format": "png", "style": {"scale": 8}}
{"data": "https://example.com/1"}
{"data": "https://example.com/2"}

Styling

The API accepts everything the generator page offers. All fields are optional and apply to every code in the job.

FieldTypeNotes
ecLevelstringL / M (default) / Q / H. Use Q or H with a logo
scaleintegerPixels per module, 1–40 — long data at large scales is capped at 4,096 px per side
marginintegerQuiet-zone modules, 0–16
foregroundstringHex colour (e.g. #1a1a2e) or transparent
backgroundstringHex colour or transparent. Foreground and background can't both be transparent
logoobjectCentre logo, see below
captionobjectText under the code, see below
FieldTypeNotes
dataUrlstring, requiredA PNG data: URL (data:image/png;base64,…), at most 512 KB. Remote URLs are rejected
sizeRationumberLogo edge as a fraction of the QR edge, 0.05–0.3 (default 0.2)

A logo hides part of the code, so raise error correction (ecLevel: "Q" or "H") and keep the ratio modest — the defaults stay comfortably scannable.

Captions

FieldTypeNotes
textstring, requiredUp to 120 characters, rendered beneath the code
colourstringHex colour; defaults to the foreground colour
fontSizeinteger6–64 (default 16), scaled with the image

Per-item text: items[].caption overrides the job caption for that code — and works without a job-level caption. Filenames follow the same per-item pattern: items[].filename names the file (up to 120 characters), otherwise a name is derived from the data. Names are sanitised and truncated to 100 characters in the ZIP. JSON jobs suffix a name only when it collides with another item's; streaming (NDJSON) jobs append the item's index to every filename.

Example

curl -X POST https://bulkqr.com/api/v1/jobs \
  -H "Authorization: Bearer $BULKQR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{"data": "https://example.com", "caption": "Table 1"}],
    "format": "both",
    "style": {
      "ecLevel": "Q",
      "background": "transparent",
      "logo": {"dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGN4xqANAALhARJlKxA9AAAAAElFTkSuQmCC"}
    }
  }'

That data URL is a valid 1×1 PNG — handy for trying the API before you export a real logo.

Status — GET /api/v1/jobs/{jobId}

{"id": "…", "status": "processing", "total": 50000, "completed": 31200, "failed": 0}

status is processingzippingcomplete (or failed). downloadUrl appears once complete.

Download — GET /api/v1/jobs/{jobId}/download

Streams the ZIP; Range requests are supported. Results are retained for 90 days.

Credits and the free tier

Jobs of up to 100 items are free. Larger jobs cost one credit per item, debited from the organisation's balance when the job is accepted — buy credits on the account page. A job that can't be paid for is rejected with 402 insufficient_credits and costs nothing.

Rate limits

EndpointLimit (per key)
Create job10 / minute
Status & download60 / minute

Over the limit, responses are 429 with a Retry-After header.

Errors

Errors are JSON: {"error": "snake_case_code", …}.

StatusCodeMeaning
400invalid_inputValidation failed; an issues array pinpoints the fields
400data_too_long / qr_too_largeAn item exceeds QR capacity, or data × scale exceeds the pixel cap
400invalid_json_lineNDJSON line is not valid JSON
400invalid_meta_lineNDJSON first line is missing or isn't a valid job meta object
400too_many_itemsNDJSON body exceeds the item cap
401invalid_api_keyMissing, malformed, or revoked key
402insufficient_creditsTop up on the account page
404not_foundNo such job in your organisation
409not_readyZIP requested before the job completed
413payload_too_largeBody over 25 MB (JSON) / 100 MB (NDJSON)
429rate_limitedHonour Retry-After