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 (nullor""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;nullremoves 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);nullremoves it.maxScans— a hard scan cap (the existing scan count is preserved);nullremoves it.preset/brandKitId— restyle from a builder preset or a saved brand kit, expanded server-side (mutually exclusive;brandKitId: nullclears the kit association).campaign— A/B split traffic across weighted destination variants (nullclears it).routingRules— route by device / OS / country (array, max 50;[]ornullclears).deepLinkConfig— platform-specific app deep links ({ ios, android };nullclears).- Styling —
dotsOptions,cornersSquareOptions,cornersDotOptions,backgroundOptions,imageOptions,image,frame,shape,qrOptions,width,height,margin,returnType. Changing any of these regenerates the stored image, so a laterGET /imagereturns the new look. Style option objects you send replace the stored ones whole-key (an omittedimagedrops the logo, an omittedframedrops the frame), butwidth,height,margin, andreturnTypeare inherited from the saved style when omitted, so apreset-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).
format—png(default),svg,webp,jpg, orpdf. 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.
Temporary download links
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.
formatandsize— 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}/resultThe 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
| Field | Type | Notes |
|---|---|---|
data | string | Required. Destination URL, or the payload for a non-URL dataType. |
dataType | string | url (default), text, email, wifi, vcard, sms, phone, geo, vcalendar, mecard, gs1. |
returnType | string | png (default), svg, jpg, jpeg, webp. |
width | integer | Rendered width in pixels. |
height | integer | Rendered height in pixels. |
margin | integer | Quiet-zone border, in modules, around the code (0 disables it). |
name | string | A label for the code in your dashboard. |
brandKitId | string | Apply 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. |
preset | string | Apply 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. |
shape | string | Code silhouette: square (default) or circle (dots arranged in a disc). |
frame | object | Decorative 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. |
sources | array | Attribution tags, up to 6. |
qrOptions
| Field | Type | Notes |
|---|---|---|
errorCorrectionLevel | string | L, M, Q, or H. Raise to Q/H when overlaying a logo. |
typeNumber | integer | Force a specific QR version. Usually leave unset (auto). |
mode | string | Encoding 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.
| Field | Type | Notes |
|---|---|---|
type | string | Body (dotsOptions): square, dots, rounded, classy, classy-rounded, extra-rounded. Eyes and eye-dots also accept dot. |
color | string or gradient | A solid hex #rrggbb, or a gradient object. |
backgroundOptions
| Field | Type | Notes |
|---|---|---|
color | string or gradient | Solid hex, a gradient, or transparent. |
round | number | Corner rounding of the background panel, 0 (square) to 1 (fully rounded). |
imageOptions and image
| Field | Type | Notes |
|---|---|---|
image | string | The center logo, as a base64 / data URI. Up to 1 MB. |
imageOptions.imageSize | number | Fraction of the code the logo occupies, 0 to 1. |
imageOptions.margin | number | Padding, in modules, between the logo and surrounding dots. |
imageOptions.hideBackgroundDots | boolean | Clear 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.
| Field | Type | Notes |
|---|---|---|
type | string | linear or radial. |
rotation | number | Angle in radians (linear gradients). 0 is left-to-right. |
colorStops | array | [{ 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.
| Field | Type | Notes |
|---|---|---|
secure | array | Any of password, encrypt, expire. |
password | string | Required when secure includes password. |
encryption | object | { type: 'AES' | 'RSA', key }. Required when secure includes encrypt. |
expiry | integer | Expiry 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
}
}