Skip to main content
ER ExaRoutes

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

FieldTypeDefaultNotes
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

FieldTypeAllowed valuesNotes
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

FieldTypeNotes
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

FieldAllowed 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

StatusWhen
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.