Generate QR
Create a single QR code. The same endpoint produces static or dynamic QRs
depending on qrType. The response carries the rendered image
inline. There is no separate fetch step.
Endpoint
POST https://api.exaroutes.com/api/qr/generate
Both auth headers required.
Content-Type: application/json. Max body: 25 MB.
Top-level fields
| Field | Type | Default | Notes |
|---|---|---|---|
data (required) | string | object | (none) | Payload. String for text/url/phone/geo, object for the structured types. See Data shapes. |
qrType | "static" | "dynamic" | static | static bakes data directly into the QR (immutable). dynamic encodes a short URL qr.exaroutes.com/r/<uid> that you can re-point later and that records every scan. |
dataType | enum | text | One of text, url, email, phone, SMS, geo, wifi, vcard, vcalendar, mecard. url rejects non-http(s) schemes. |
returnType | enum | png | One of png, svg, jpg, jpeg, webp. |
width | integer | 256 | Pixels. Hard cap 4096. Larger values return 400. |
height | integer | 256 | Pixels. Hard cap 4096. |
margin | integer | 4 | Quiet-zone padding in modules around the QR. Must be numeric. |
shape | "square" | "circle" | square | Outer QR silhouette. |
image | string | (none) | Logo URL or data: URL. Centered on the QR. Max 1 MB base64. |
uid | string | auto | Client-supplied identifier echoed back in the response. Required on every row of bulk-generate so you can match results back to inputs. |
qrOptions | object | (none) | Encoding options. See qrOptions. |
imageOptions | object | (none) | Logo placement. See imageOptions. |
dotsOptions | object | (none) | Body-module styling. See Styling options. |
backgroundOptions | object | (none) | Background fill. See Styling options. |
cornersSquareOptions | object | (none) | Outer locator-square style. See Styling options. |
cornersDotOptions | object | (none) | Inner locator-dot style. See Styling options. |
sources | string[] | (none) | Multi-variant QR (one image per source name, with ?source=<name> appended to the redirect URL). Dynamic only. Max 6, no duplicates. |
secure | ("password" | "encrypt" | "expire")[] | (none) | Enable secure QR features. Dynamic only. See Secure QRs. |
password | string | (none) | Required when secure includes "password". Scanners must enter this on the unlock screen before the redirect fires. |
encryption | object | (none) | Required when secure includes "encrypt". { "type": "AES" | "RSA", "key"?: string }. key is required for AES. |
expiry | integer (epoch ms) | (none) | Required when secure includes "expire". Must be a finite number in the future. After expiry the redirect handler returns 410 Gone. |
Data shapes per dataType
String-typed payloads pass through as-is. Object-typed payloads get serialised to the canonical scanner format (WIFI:, BEGIN:VCARD, etc.) on the server.
text / url
"data": "https://example.com/landing"
url additionally enforces http:// or https://. Other schemes are rejected with 400.
email
"data": {
"email": "alice@example.com",
"subject": "Hello",
"body": "..."
}
Serialised to mailto:<email>?subject=<subject>&body=<body>.
phone
"data": "+15551234567"
Serialised to tel:<data>. Must be a string, not an object.
SMS
"data": {
"phone": "+15551234567", // alias: "tel"
"message": "Hello!" // alias: "body"
}
Static QRs serialise to SMSTO:<tel>:<body>. Dynamic QRs use sms:<tel>?body=<body>.
geo
"data": "37.7749,-122.4194"
Serialised to geo:<data>. Must be a string.
wifi
"data": {
"ssid": "OfficeGuest", // alias: "name"
"password": "letmein",
"encryption": "WPA", // "WPA" | "WEP" | "" (open). Alias: "type"
"hidden": false // optional, true for hidden SSIDs
}
Serialised to WIFI:T:<encryption>;S:<ssid>;P:<password>;H:<hidden>;.
vcard
vCard 3.0. Every field below is optional. Empty fields are omitted from the serialised output (no naked ORG: lines).
"data": {
"name": "Alice Doe", // OR firstName + lastName below
"firstName": "Alice",
"lastName": "Doe",
"org": "Acme Corp", // alias: "company"
"title": "Head of Marketing",
"tel": "+15551234567", // alias: "phone"
"email": "alice@example.com",
"url": "https://acme.com",
"address": "1 Market St, San Francisco CA",
"note": "Met at QR-Con 2026"
}
vcalendar
iCalendar (RFC 5545). Date-time fields use YYYYMMDDTHHMMSSZ (UTC, no separators).
"data": {
"eventStart": "20260615T140000Z",
"eventEnd": "20260615T150000Z",
"eventSummary": "Product launch demo",
"eventLocation": "Booth 42",
"eventDescription": "Demo + Q&A"
}
mecard
"data": {
"fName": "Alice",
"lName": "Doe",
"tel": "+15551234567",
"email": "alice@example.com",
"address": "1 Market St",
"birthDay": "19900615",
"nickName": "Ali",
"note": "...",
"url": "https://acme.com"
}
qrOptions
Field Type Allowed values Notes errorCorrectionLevel enum "L" | "M" | "Q" | "H" Reed-Solomon ECC level (~7% / 15% / 25% / 30% damage tolerance). Higher = denser QR. Default M. typeNumber integer 0 to 40 QR version (size). 0 = auto-pick smallest that fits the payload. mode enum "Numeric" | "Alphanumeric" | "Byte" | "Kanji" Encoding mode. Auto-detected when omitted.
imageOptions
Field Type Notes hideBackgroundDots boolean Hide the QR modules behind the logo so the logo sits on a clean background. Default false. imageSize number Logo size relative to the QR (typically 0 to 1, e.g. 0.4 = 40% of QR width). margin number Padding around the logo, in modules.
Styling options
dotsOptions, backgroundOptions,
cornersSquareOptions, and cornersDotOptions share
the same shape: a type (where applicable), plus exactly one of
color or gradient.
Allowed type values
Field Allowed type dotsOptions.type "square", "dots", "rounded", "extra-rounded", "classy", "classy-rounded" cornersSquareOptions.type "square", "dot", "dots", "rounded", "extra-rounded", "classy", "classy-rounded" cornersDotOptions.type "square", "dot", "dots", "rounded", "extra-rounded", "classy", "classy-rounded" backgroundOptions (no type. Supports round: number, the corner rounding fraction)
Solid fill
"dotsOptions": { "type": "rounded", "color": "#1a1a1a" }
Gradient fill
"dotsOptions": {
"type": "rounded",
"gradient": {
"type": "linear", // "linear" | "radial"
"rotation": 0.785, // radians; only used by linear gradients
"colorStops": [
{ "offset": 0, "color": "#ea580c" },
{ "offset": 1, "color": "#7c3aed" }
]
}
}
Each color-stop must have offset (number, 0 to 1) and
color (string). At least one stop is required. Specify either
color or gradient, not both. The
renderer only honours one.
Secure QRs (secure)
Combine any of "password", "encrypt", and
"expire" in the secure array. Only
dynamic QRs support secure features. Passing secure
with qrType: "static" returns 400.
Password gate
{
"qrType": "dynamic",
"dataType": "url",
"data": "https://example.com/private",
"secure": ["password"],
"password": "trade-show-2026"
}
The unlock screen at qr.exaroutes.com/r/<uid> challenges the scanner before the redirect fires.
Encryption
// AES-256 — caller supplies the key.
{
"secure": ["encrypt"],
"encryption": { "type": "AES", "key": "<32-byte-base64-key>" }
}
// RSA — server generates the keypair; the response includes the
// private key once. Store it; you cannot retrieve it later.
{
"secure": ["encrypt"],
"encryption": { "type": "RSA" }
}
On encrypted-QR responses, the API returns the IV (AES) or the private key
(RSA) on the response root: { ..., "iv": "..." } or
{ ..., "encryptionKey": "..." }.
Expiry
{
"secure": ["expire"],
"expiry": 1781827200000 // unix epoch ms; must be in the future
}
After the deadline the public redirect handler responds 410 Gone.
Example: dynamic URL QR with branding
curl https://api.exaroutes.com/api/qr/generate \
-H "client-id: cust_abc123…" \
-H "api-key: sk_live_xyz789…" \
-H "Content-Type: application/json" \
-d '{
"qrType": "dynamic",
"dataType": "url",
"data": "https://example.com/landing",
"returnType": "png",
"width": 512,
"height": 512,
"qrOptions": { "errorCorrectionLevel": "Q" },
"dotsOptions": { "type": "rounded", "color": "#1a1a1a" },
"cornersSquareOptions": { "type": "extra-rounded", "color": "#ea580c" },
"cornersDotOptions": { "type": "dot", "color": "#ea580c" },
"backgroundOptions": { "color": "#ffffff" },
"image": "https://cdn.example.com/logo.png",
"imageOptions": { "hideBackgroundDots": true, "imageSize": 0.35, "margin": 4 }
}'
Example: static WiFi QR (SVG)
curl https://api.exaroutes.com/api/qr/generate \
-H "client-id: …" -H "api-key: …" -H "Content-Type: application/json" \
-d '{
"qrType": "static",
"dataType": "wifi",
"data": {
"ssid": "OfficeGuest",
"password": "letmein",
"encryption": "WPA",
"hidden": false
},
"returnType": "svg",
"width": 1024
}'
Response
The response shape depends on qrType.
Static QRs
JSON-encoded string body: the rendered image as a data: URL
(PNG/JPG/WebP) or a compressed SVG string.
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…"
Dynamic QRs
{
"id": "qr_abc123…", // DynamicQr row id; used by /api/qr/dynamic-qr
"uid": "h4k2x9", // short id; the redirect path is /r/<uid>
"url": "https://qr.exaroutes.com/r/h4k2x9",
"QRs": "data:image/png;base64,…"
}
Dynamic QRs with sources
QRs becomes a map of one rendered image per source name:
{
"id": "qr_abc123…",
"uid": "h4k2x9",
"url": "https://qr.exaroutes.com/r/h4k2x9",
"QRs": {
"instagram": "data:image/png;base64,…?source=instagram",
"tiktok": "data:image/png;base64,…?source=tiktok"
}
}
Encrypted dynamic QRs
An additional field is included on the response root:
- AES:
"iv": "<base64-iv>", needed alongside your key to decrypt. - RSA:
"encryptionKey": "<PEM private key>", shown once. Store it.
Errors
Status When 400Missing or invalid field. The error message names which one (e.g. "Invalid dotsOptions -> type provided."). 400"URL must use http:// or https://": non-web scheme on dataType: url. 400"Width too large (max 4096px)." / "Image is too large (max 1 MB base64)." 400"Only `dynamic` type QRs support sources / secure features": passed sources or secure with qrType: "static". 400"Expiry must be in the future." 401 / 403 / 404Auth. See Authentication. 429Plan QR-creation limit reached. The error body names the saturated quota (e.g. "Plan limit reached: dynamic_qr_count (5/5)."). 500Render or storage failure. The DynamoDB row is rolled back automatically. Safe to retry.
Routing rules, campaigns, deep linking
The redirect handler can route to different destinations based on country,
device, OS, A/B campaign weights, or platform deep-links. These are
configured via the dashboard / session API on individual QRs (PUT
/api/qr/dynamic-qr). They are not part of the create payload. The
fields persist on the QR row:
routingRules[]: { id, condition: { country?: string[], device?: ("mobile"|"tablet"|"desktop")[], os?: ("iOS"|"Android"|"Windows"|"Mac OS"|"Linux")[] }, destination }. Country is ISO 3166-1 alpha-2, max 50 per rule. First match wins. campaign: { status: "active"|"paused"|"completed", variants: [{ id, name, destination, weight }], winnerId? }. Up to 20 variants. winnerId pins 100% to one variant. deepLinkConfig: { ios?, android? }. iOS Universal Link / App Store URL. Android App Link / intent:// / Play Store URL. maxScans: integer hard cap. After scanCount >= maxScans the redirect returns 410 Gone.
Updating a dynamic QR's destination requires step-up MFA (5-minute grace,
see Authentication) because changing
where a printed QR points is a defacement-class action.