API referenceQR endpoints

QR endpoints

The customer API has two parts:

  • /api/qr/codes: a resource API for dynamic QR codes. Create, list, read, update, deactivate, download images (any format, including vector PDF), mint temporary download links, and read scan analytics. Dynamic codes route every scan through ExaRoutes, so they are editable and tracked.
  • /api/qr/static: a one-shot renderer for static QR codes. Static codes encode the payload directly, so there is nothing to track or edit. Nothing is stored.

Every endpoint here is authenticated with a Bearer API key. See Authentication.

The id used throughout is the QR code’s shortId (the redirect slug). It is the only identifier the API exposes.

Create a dynamic QR code

POST /api/qr/codes
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "data": "https://yourcompany.com/spring",
  "dataType": "url",
  "returnType": "png"
}

data is required. dataType defaults to url (other values: text, email, wifi, vcard, sms, phone, geo, vcalendar, mecard, gs1). returnType (png, svg, jpg, jpeg, or webp) sets the format of the printable image. Every styling field below is optional; see Styling request body for the full design surface (gradients, per-eye colors, logo, error correction, and more).

Response:

{
  "id": "Ab3xK9pQ",
  "type": "dynamic",
  "name": "Spring poster",
  "dataType": "url",
  "destination": "https://yourcompany.com/spring",
  "redirectUrl": "https://qr.exaroutes.com/r/Ab3xK9pQ",
  "status": "active",
  "encrypted": false,
  "passwordProtected": false,
  "expiresAt": null,
  "maxScans": null,
  "scanCount": 0,
  "secure": [],
  "createdAt": "2026-05-21T15:00:00.000Z",
  "updatedAt": "2026-05-21T15:00:00.000Z"
}

The QR pattern encodes redirectUrl. Fetch the printable image separately (see below).

List QR codes

GET /api/qr/codes?limit=20&status=active
Authorization: Bearer exr_live_...

Query parameters: limit (1-100, default 25), cursor (from a prior response), type (filter by dataType), status (active or inactive).

{
  "data": [ { "id": "Ab3xK9pQ", "type": "dynamic", "...": "..." } ],
  "pagination": { "total": 42, "limit": 20, "cursor": "0", "nextCursor": "20" }
}

Pass nextCursor back as cursor to fetch the next page. nextCursor is null on the last page.

Get one QR code

GET /api/qr/codes/Ab3xK9pQ
Authorization: Bearer exr_live_...

Returns a single QR code object, the same shape as the create response.

Update a QR code

PUT /api/qr/codes/{id} updates any subset of a dynamic code. Send only the fields you want to change. The printed id never changes.

PUT /api/qr/codes/Ab3xK9pQ
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "destination": "https://yourcompany.com/spring-v2"
}

You can update far more than the destination:

  • name — the dashboard label (null or "" clears it).
  • status"active" or "inactive". Inactive codes stop redirecting (scans get a branded page) and can be flipped back at any time.
  • password — a string sets or replaces the scan password; null removes protection.
  • expiresAt — ISO 8601 or epoch milliseconds; must be in the future and within the JS date range (epoch ms up to 8,640,000,000,000,000); null removes it.
  • maxScans — a hard scan cap (the existing scan count is preserved); null removes it.
  • preset / brandKitId — restyle from a builder preset or a saved brand kit, expanded server-side (mutually exclusive; brandKitId: null clears the kit association).
  • campaign — A/B split traffic across weighted destination variants (null clears it).
  • routingRules — route by device / OS / country (array, max 50; [] or null clears).
  • deepLinkConfig — platform-specific app deep links ({ ios, android }; null clears).
  • StylingdotsOptions, cornersSquareOptions, cornersDotOptions, backgroundOptions, imageOptions, image, frame, shape, qrOptions, width, height, margin, returnType. Changing any of these regenerates the stored image, so a later GET /image returns the new look. Style option objects you send replace the stored ones whole-key (an omitted image drops the logo, an omitted frame drops the frame), but width, height, margin, and returnType are inherited from the saved style when omitted, so a preset-only restyle never shrinks a print-sized code back to defaults.
PUT /api/qr/codes/Ab3xK9pQ
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "campaign": {
    "status": "active",
    "variants": [
      { "id": "a", "name": "Landing A", "destination": "https://x.com/a", "weight": 1 },
      { "id": "b", "name": "Landing B", "destination": "https://x.com/b", "weight": 1 }
    ]
  },
  "routingRules": [
    { "id": "r1", "condition": { "country": ["DE"] }, "destination": "https://x.com/de" }
  ],
  "frame": { "id": "scanme", "text": "Scan me", "color": "#2563eb" }
}

The response echoes back the QR with its campaign, routingRules, deepLinkConfig, and style (only the ones that are set). Encrypted and compliance-locked codes cannot be updated via the API (409); edit those from the dashboard. Password-protected codes are fully editable here since the API manages the password itself.

Deactivate a QR code

DELETE /api/qr/codes/Ab3xK9pQ
Authorization: Bearer exr_live_...

Deactivate, not destroy: scans of an inactive code get a branded page, while analytics, styling, and configuration are all preserved. PUT { "status": "active" } reactivates. Idempotent; returns the (now inactive) QR object.

Read scan analytics

GET /api/qr/codes/Ab3xK9pQ/analytics?days=30
Authorization: Bearer exr_live_...

days is 1-90 (default 30). Returns a day-wise scan time series and a country breakdown.

Get the printable image

GET /api/qr/codes/Ab3xK9pQ/image?format=pdf&size=1024
Authorization: Bearer exr_live_...

Renders the code on demand from its saved style, so every format is faithful to the design (frame and caption included).

  • formatpng (default), svg, webp, jpg, or pdf. PDF is a vector-first conversion of the SVG render, suitable for print.
  • size — optional square pixel override for width and height, 64 to 4096.
GET /api/qr/codes/Ab3xK9pQ/download-url?format=svg&ttl=86400
Authorization: Bearer exr_live_...

Mints a time-limited URL that serves the image with no authentication: safe to paste into an <img> tag, hand to a print vendor, or embed in an email without exposing your API key. The link is HMAC-signed over the code, format, size, and expiry; changing any of them invalidates it.

  • format and size — same as the image endpoint.
  • ttl — link lifetime in seconds, 60 to 86400 (default 3600).
{
  "url": "https://api.exaroutes.com/api/qr/render/Ab3xK9pQ?format=svg&exp=1721930000000&sig=...",
  "format": "svg",
  "expiresAt": "2026-07-25T18:00:00.000Z"
}

The link responds with the image bytes, allows cross-origin embedding, and returns a plain 404 once expired. Deactivated codes still render (the image only encodes the redirect URL), so a printed-then-paused code can be re-downloaded.

Bulk create

Each row is the full create body (styling, preset/brandKitId, protection, structured payloads) plus a required uid, echoed back in the result so you can match rows to outcomes. See the bulk format guide for the complete contract.

POST /api/qr/codes/bulk
Authorization: Bearer exr_live_...
Content-Type: application/json
 
[
  { "uid": "row-1", "data": "https://yourcompany.com/a", "preset": "cafe" },
  { "uid": "row-2", "data": "https://yourcompany.com/b", "brandKitId": "bk_abc123" }
]

Bulk creation is asynchronous (up to 1,000 rows per call). A bad preset or brandKitId fails the whole call synchronously with a 400 naming the row, before any job is queued. The response is a bulkId. Poll its status:

GET /api/qr/codes/bulk/{bulkId}

When the job is done, download the result archive once:

GET /api/qr/codes/bulk/{bulkId}/result

The result is a one-shot download; it is removed after the first successful fetch.

Render a static QR code

POST /api/qr/static
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "data": "https://yourcompany.com",
  "dataType": "url",
  "returnType": "svg"
}

Static codes are not stored and have no id, no analytics, and no destination updates. The rendered image is returned inline:

{
  "type": "static",
  "dataType": "url",
  "format": "svg",
  "image": "<svg ...>...</svg>"
}

For returnType: png the image field is a data: URI.

Styling request body

Both POST /api/qr/codes and POST /api/qr/static accept the full styling surface below. Every field is optional and validated server-side before it is forwarded to the renderer. Fields are the same for dynamic and static codes, except secure (dynamic only). For design guidance, see the customization guide.

Top-level fields

FieldTypeNotes
datastringRequired. Destination URL, or the payload for a non-URL dataType.
dataTypestringurl (default), text, email, wifi, vcard, sms, phone, geo, vcalendar, mecard, gs1.
returnTypestringpng (default), svg, jpg, jpeg, webp.
widthintegerRendered width in pixels.
heightintegerRendered height in pixels.
marginintegerQuiet-zone border, in modules, around the code (0 disables it).
namestringA label for the code in your dashboard.
brandKitIdstringApply a saved brand kit: its full captured style (colors, gradients, eyes, frame, logo) is expanded server-side and the kit stays associated. Mutually exclusive with preset.
presetstringApply a builder look or industry preset, expanded server-side to the same pixels as the dashboard: classic, soft, dots, ocean, sunset, contrast-eyes, menu, bizcard, wifi, event, boutique, cafe. Explicit style fields you send alongside win.
shapestringCode silhouette: square (default) or circle (dots arranged in a disc).
frameobjectDecorative frame + caption. { id, text, color, textColor, background, font, uppercase }id one of none, border, scanme, banner, rounded, polaroid, phone, ticket, bubble, ribbon; font one of sans, serif, mono, display, rounded. See Frames.
sourcesarrayAttribution tags, up to 6.

qrOptions

FieldTypeNotes
errorCorrectionLevelstringL, M, Q, or H. Raise to Q/H when overlaying a logo.
typeNumberintegerForce a specific QR version. Usually leave unset (auto).
modestringEncoding mode. Usually leave unset (auto).

dotsOptions, cornersSquareOptions, cornersDotOptions

These three objects style the body modules, the three finder eyes, and the inner dot of each eye, respectively. They share the same shape.

FieldTypeNotes
typestringBody (dotsOptions): square, dots, rounded, classy, classy-rounded, extra-rounded. Eyes and eye-dots also accept dot.
colorstring or gradientA solid hex #rrggbb, or a gradient object.

backgroundOptions

FieldTypeNotes
colorstring or gradientSolid hex, a gradient, or transparent.
roundnumberCorner rounding of the background panel, 0 (square) to 1 (fully rounded).

imageOptions and image

FieldTypeNotes
imagestringThe center logo, as a base64 / data URI. Up to 1 MB.
imageOptions.imageSizenumberFraction of the code the logo occupies, 0 to 1.
imageOptions.marginnumberPadding, in modules, between the logo and surrounding dots.
imageOptions.hideBackgroundDotsbooleanClear the dots behind the logo so it reads cleanly.

gradient

Any color slot (dots, corner-square, corner-dot, or background) accepts a gradient in place of a solid color.

FieldTypeNotes
typestringlinear or radial.
rotationnumberAngle in radians (linear gradients). 0 is left-to-right.
colorStopsarray[{ offset, color }], where offset runs 0 to 1. Two or more stops.

secure (dynamic only)

Password, encryption, and expiry stack. See the customization guide for behavior.

FieldTypeNotes
securearrayAny of password, encrypt, expire.
passwordstringRequired when secure includes password.
encryptionobject{ type: 'AES' | 'RSA', key }. Required when secure includes encrypt.
expiryintegerExpiry time as an epoch-ms timestamp. Required when secure includes expire.

Full example

A dynamic code with a linear gradient body, distinct finder-eye colors, a dot inner corner dot, a transparent background, and a center logo:

POST /api/qr/codes
Authorization: Bearer exr_live_...
Content-Type: application/json
 
{
  "data": "https://yourcompany.com/spring",
  "dataType": "url",
  "returnType": "png",
  "width": 1024,
  "height": 1024,
  "margin": 16,
  "qrOptions": { "errorCorrectionLevel": "H" },
  "dotsOptions": {
    "type": "rounded",
    "color": {
      "type": "linear",
      "rotation": 0.79,
      "colorStops": [
        { "offset": 0, "color": "#4f46e5" },
        { "offset": 1, "color": "#0ea5e9" }
      ]
    }
  },
  "cornersSquareOptions": { "type": "extra-rounded", "color": "#4f46e5" },
  "cornersDotOptions": { "type": "dot", "color": "#0ea5e9" },
  "backgroundOptions": { "color": "transparent", "round": 0.2 },
  "image": "data:image/png;base64,iVBORw0KGgo...",
  "imageOptions": {
    "hideBackgroundDots": true,
    "imageSize": 0.3,
    "margin": 4
  }
}