API referenceSerialized codes

Serialized codes

Serialized (anti-counterfeit) codes mint a unique, cryptographically signed QR per physical unit. The first scan of a code returns “authentic”; every later scan flags “already scanned”, so a copied label is detectable. Codes verify forever.

This API manages serialized batches with a Bearer API key. See Authentication. Every route is scoped to your key’s workspace.

Requires the Business plan (the serializedQR capability). Minting is metered against your serialized quota; buying more codes is done in the dashboard, not the API (see Buying more codes).

Minting large batches is asynchronous: the mint call returns a jobId that you poll until the codes are written. Image-pack exports work the same way.

Check your quota

GET /api/qr/serialized-batches/consumption
Authorization: Bearer exr_live_...
{
  "serializedEnabled": true,
  "apiName": "qr.business",
  "allotment": 1000,
  "purchased": 10000,
  "minted": 4200,
  "total": 11000,
  "remaining": 6800
}

total is your capacity (allotment included with the plan, plus any purchased blocks). remaining is total - minted. A mint is rejected once remaining reaches zero.

Mint a batch

POST /api/qr/serialized-batches
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "batchName": "Spring 2026 production run",
  "count": 5000,
  "productSku": "ACME-1234",
  "productName": "ACME Premium Widget",
  "gtin": "00012345678905"
}

batchName (max 120 chars) and count are required. count must be between 1 and 50,000 and must fit your remaining quota. productSku, productName, and gtin are optional (max 200 chars each) and are shown on the verification page when a customer scans.

Send an optional Idempotency-Key header (any string up to 200 chars): retrying a mint with the same key returns the same job instead of minting twice. Recommended for any automated integration.

Response (the job was accepted, not finished):

{
  "jobId": "sjob_a1b2c3d4e5f6",
  "batchId": "batch_9y8x7w6v5u",
  "count": 5000
}

A 402 means the count exceeds your remaining quota. Buy more codes in the dashboard.

Poll a job

Poll until status is completed (or failed). Works for both mint jobs and image-export jobs.

GET /api/qr/serialized-batches/jobs/sjob_a1b2c3d4e5f6
Authorization: Bearer exr_live_...
{
  "jobId": "sjob_a1b2c3d4e5f6",
  "jobType": "mint",
  "status": "processing",
  "count": 5000,
  "itemsWritten": 2500,
  "batchId": "batch_9y8x7w6v5u",
  "imageFormat": null,
  "resultReady": false,
  "createdAt": 1717000000000,
  "updatedAt": 1717000030000
}

status is one of pending, processing, completed, failed. itemsWritten lets you show progress; imageFormat is set on image-export jobs. If a mint job fails, the reserved quota is refunded automatically.

List batches

GET /api/qr/serialized-batches
Authorization: Bearer exr_live_...
{
  "batches": [
    {
      "batchId": "batch_9y8x7w6v5u",
      "batchName": "Spring 2026 production run",
      "count": 5000,
      "scanned": 128,
      "status": "completed",
      "productSku": "ACME-1234",
      "productName": "ACME Premium Widget",
      "gtin": "00012345678905",
      "createdAt": 1717000000000
    }
  ]
}

Get a batch and its items (paginated)

Returns the batch summary plus one page of items, including each item’s verification URL and first-scan status. Query params: limit (1-500, default 500) and cursor (the nextCursor from the previous page). For the full item set in one go, use the CSV export instead.

GET /api/qr/serialized-batches/batch_9y8x7w6v5u?limit=500
Authorization: Bearer exr_live_...
{
  "batchId": "batch_9y8x7w6v5u",
  "batchName": "Spring 2026 production run",
  "count": 5000,
  "scanned": 128,
  "status": "completed",
  "productSku": "ACME-1234",
  "productName": "ACME Premium Widget",
  "gtin": "00012345678905",
  "createdAt": 1717000000000,
  "items": [
    {
      "itemId": "a1b2c3d4e5f6a7b8",
      "url": "https://qr.exaroutes.com/v/a1b2c3d4e5f6a7b8/9f8e...",
      "productSku": "ACME-1234",
      "productName": "ACME Premium Widget",
      "scanCount": 1,
      "firstScannedAt": 1717100000000,
      "firstScanCountry": "US"
    }
  ],
  "nextCursor": "eyJpdGVtSWQiOiJ..."
}

nextCursor is null on the last page. The url on each item is what you print as a QR; scanning it hits the hosted verification page.

Export the item list as CSV

GET /api/qr/serialized-batches/batch_9y8x7w6v5u/items.csv
Authorization: Bearer exr_live_...

Streams the whole batch as text/csv with columns itemId,url,productSku,productName. This is the easiest way to hand the verification URLs to a label printer.

Export print-ready images

Renders one QR image per code and packages them as a ZIP. Asynchronous: start the export, poll the returned jobId, then download when it is ready.

POST /api/qr/serialized-batches/batch_9y8x7w6v5u/images
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{ "format": "svg" }

format is svg (default, vector, best for print) or png. Response:

{ "jobId": "szip_1a2b3c4d5e6f", "batchId": "batch_9y8x7w6v5u", "format": "svg", "count": 5000 }

A 409 means the batch is still minting; wait for the mint job to complete first.

Poll the job (see Poll a job) until resultReady is true, then download:

GET /api/qr/serialized-batches/batch_9y8x7w6v5u/images/download?jobId=szip_1a2b3c4d5e6f
Authorization: Bearer exr_live_...

The response is the ZIP file (application/zip), one image per code named by item id. The export stays downloadable for 30 days, so you can fetch it again without re-rendering.

Verifying a scan

Verification is what your customers do by scanning the printed QR. Each item’s url (https://qr.exaroutes.com/v/{itemId}/{signature}) is a hosted page: the first scan claims the item and shows “authentic”, later scans show “already scanned”. You do not call anything. See the rate limits for the public verification endpoints.

To react to scans in your own systems, subscribe a webhook to serialized.first_scanned (a genuine unit’s first scan) and serialized.duplicate_scanned (a scan of an already-claimed code, i.e. a possible counterfeit).

Buying more codes

Serialized codes are sold as one-time prepaid blocks (1,000 / 10,000 / 100,000). Purchases go through a secure checkout in the dashboard, so buying is not exposed over the API. Once a block is purchased, the new capacity appears in consumption and you can mint against it immediately.