API Docs/Guides/Dynamic QR Codes · Node.js

Dynamic QR Codes with Node.js and Fastify

Static QR codes are a dead end — once printed, the destination is frozen forever. Dynamic QR codes fix that. This guide walks through the Qrius REST API so you can create, update, and manage QR codes programmatically from a Node.js backend.

Static vs. dynamic: the actual difference

A static QR code encodes the destination URL directly into the image. Change the URL, and you need a new code — and new printouts. That's fine for a personal business card. It's a nightmare for a campaign that runs across 10,000 printed brochures.

A dynamic QR code encodes a short redirect URL (like https://qrius.io/summer-2025) that you control. The printed image never changes. The destination can. Update it from the dashboard or via API — the codes already in the field pick it up on the next scan.

Get your API key

Every request to the Qrius API needs a Bearer token. Log into your dashboard, go to Settings → API Keys, and create one. It'll look like qrius_xxxxxxxxxxxx.

All requests go to https://qrius.io/api. The auth header is the same everywhere:

Authorization: Bearer qrius_YOUR_API_KEY

Create a QR code

POST /api/qr with a JSON body. Only name and destinationUrl are required.

curl

curl -X POST https://qrius.io/api/qr \
  -H "Authorization: Bearer qrius_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer 2025 Campaign",
    "destinationUrl": "https://example.com/summer-2025",
    "slug": "summer-2025",
    "description": "Printed on 10k brochures",
    "tags": ["campaign", "print"]
  }'

Node.js (fetch)

const res = await fetch('https://qrius.io/api/qr', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer qrius_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Summer 2025 Campaign',
    destinationUrl: 'https://example.com/summer-2025',
    slug: 'summer-2025',
    tags: ['campaign', 'print'],
  }),
})

const { success, data } = await res.json()

if (success) {
  console.log(data.redirectUrl) // https://qrius.io/summer-2025
  console.log(data.id)          // store this — you'll need it for PATCH
}

Response

{
  "success": true,
  "data": {
    "id": "clx1abc23def456",
    "name": "Summer 2025 Campaign",
    "slug": "summer-2025",
    "destinationUrl": "https://example.com/summer-2025",
    "redirectUrl": "https://qrius.io/summer-2025",
    "totalScans": 0,
    "isPaused": false,
    "createdAt": "2025-06-01T09:00:00.000Z"
  }
}

Store data.id — you'll need it for updates and deletes. The redirectUrl is what you encode into the QR image.

Custom slugs — and what happens when they conflict

Slugs must be 3–50 characters, lowercase alphanumeric plus hyphens (a-z0-9-). If you omit slug, the API generates one from your QR code name via slugify, with a nanoid(8) fallback for uniqueness. Short, clean, predictable.

If your chosen slug is already taken by another QR code in your account — or anyone else's — you get a 409 Conflict. Handle it:

const res = await fetch('https://qrius.io/api/qr', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer qrius_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'My QR', destinationUrl: 'https://example.com', slug: 'shop' }),
})

if (res.status === 409) {
  // slug is taken — retry without it and let the API generate one
  const retry = await fetch('https://qrius.io/api/qr', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer qrius_YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'My QR', destinationUrl: 'https://example.com' }),
  })
  const { data } = await retry.json()
  console.log(data.slug) // e.g. "my-qr-k3n8xp2q"
}

Update the destination URL

This is the whole point of dynamic QR codes. Use PATCH /api/qr/:id with the same body shape as the create endpoint. You can update any field — or just the one you care about. The QR image you've already printed doesn't change. Scans start going to the new destination immediately.

// Campaign ended — point to the archive page
const res = await fetch('https://qrius.io/api/qr/clx1abc23def456', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer qrius_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    destinationUrl: 'https://example.com/summer-2025-archive',
    name: 'Summer 2025 Campaign (archived)',
  }),
})

const { success, data } = await res.json()
console.log(data.destinationUrl) // https://example.com/summer-2025-archive

You can also pause a QR code entirely: isPaused: true will make scans return a 404 until you unpause it.

List and delete QR codes

GET /api/qr returns your QR codes paginated. Pass ?page=1&limit=20 as query params. The response includes a meta object with total count and page info so you can build proper pagination.

// List — page 1, 20 per page
const res = await fetch('https://qrius.io/api/qr?page=1&limit=20', {
  headers: { 'Authorization': 'Bearer qrius_YOUR_API_KEY' },
})
const { data, meta } = await res.json()
// meta.total, meta.page, meta.totalPages

// Delete
await fetch('https://qrius.io/api/qr/clx1abc23def456', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer qrius_YOUR_API_KEY' },
})

Plan limits apply to how many QR codes you can have active at once: FREE = 5, PRO = 50, BUSINESS = 250. Deletion frees up a slot.

Handling rate limits

The API rate limits per minute: FREE = 10, PRO = 100, BUSINESS = 10,000. During beta, FREE accounts get PRO limits — so for most use cases you won't hit the wall anytime soon. But if you're batch-importing QR codes, write defensive code anyway.

A 429 response looks like this:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "details": {
      "retryAfter": 12
    }
  }
}

Here's a simple retry helper with exponential backoff that respects the retryAfter hint from the API:

async function qriusRequest(url: string, options: RequestInit, retries = 3): Promise<Response> {
  const res = await fetch(url, options)

  if (res.status === 429 && retries > 0) {
    const body = await res.json()
    const retryAfter = body?.error?.details?.retryAfter ?? 10
    console.warn(`Rate limited. Retrying in ${retryAfter}s...`)
    await new Promise((r) => setTimeout(r, retryAfter * 1000))
    return qriusRequest(url, options, retries - 1)
  }

  return res
}

// Usage
const res = await qriusRequest('https://qrius.io/api/qr', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer qrius_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Batch QR #42', destinationUrl: 'https://example.com/42' }),
})

The tradeoff with fire-and-forget batch imports is that you're more likely to hit limits if other requests are in-flight. Adding a small delay between creates (50–100 ms) is usually enough to stay well under the PRO ceiling.

Full API reference

Everything above is the core of it. For the complete endpoint reference — including analytics endpoints, webhook payloads, and error code enumerations — the OpenAPI spec is available as JSON at:

https://qrius.io/api/docs/json

Load it into Insomnia, Postman, or any OpenAPI-compatible client and you'll have request builders for every endpoint with schema validation. The interactive browser UI is at /docs if you prefer clicking around first.

Start building

Free account gets you 5 dynamic QR codes and full API access. No credit card. API key is ready the moment you register.