Engineering Guide · API Integration

Bulk QR Code Generation via REST API

There's no magic POST /qr/bulk/create endpoint. That's intentional — bulk creation would complicate error handling, partial failures, and per-code customisation. But creating 50 or 500 QR codes programmatically is straightforward once you understand the rate limits and the right concurrency pattern.

The reality: create individually, run in parallel

Every QR code is created with a standard POST /qr request. The reason there's no single bulk-create call comes down to atomicity: what should happen if 47 of your 50 creates succeed and 3 fail? Rollback everything? Return partial results? Individual creates give you clean per-item success/failure semantics — much easier to handle in client code than a mixed batch response.

The workaround is parallelism with concurrency control. Don't create them one-at-a-time in series (painfully slow) and don't fire all 500 at once (you'll hit the rate limit immediately). The right pattern is a bounded concurrency pool — 5 or so requests in flight at a time — with automatic retry on 429.

Rate limits by plan
FREE10 requests / min
PRO100 requests / min (~1.6/sec)
BUSINESS10,000 requests / min
On PRO, sustained creation at full rate = 6,000 QR codes/hour. More than enough for conference season.

Rate-limited batch creation with p-limit

p-limit is the cleanest way to cap concurrency in Node.js. It wraps each async function call in a queue that allows at most N concurrent executions. Here's a full batch creation function that handles rate limits and slug conflicts:

import pLimit from 'p-limit'

const API_BASE = 'https://api.qrius.io'
const API_KEY  = 'qrius_YOUR_API_KEY'

interface QRCreateInput {
  name: string
  destinationUrl: string
  slug?: string
  tags?: string[]
}

interface QRCreateResult {
  id: string
  slug: string
  name: string
  qrCodeUrl: string
  shortUrl: string
}

async function createQRCode(
  input: QRCreateInput,
  attempt = 1
): Promise<QRCreateResult> {
  const res = await fetch(`${API_BASE}/qr`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  })

  // Rate limited — wait and retry
  if (res.status === 429) {
    const { retryAfter } = await res.json()
    const waitMs = (retryAfter ?? 5) * 1000
    await new Promise(r => setTimeout(r, waitMs))
    return createQRCode(input, attempt + 1)
  }

  // Slug conflict — generate a new one and retry once
  if (res.status === 409 && input.slug && attempt === 1) {
    return createQRCode(
      { ...input, slug: `${input.slug}-${Date.now().toString(36)}` },
      2
    )
  }

  if (!res.ok) {
    throw new Error(`Create failed: ${res.status} for ${input.name}`)
  }

  return res.json()
}

async function bulkCreateQRCodes(
  items: QRCreateInput[],
  concurrency = 5
): Promise<{ results: QRCreateResult[]; errors: { input: QRCreateInput; error: string }[] }> {
  const limit = pLimit(concurrency)
  const results: QRCreateResult[] = []
  const errors: { input: QRCreateInput; error: string }[] = []

  await Promise.all(
    items.map((item) =>
      limit(async () => {
        try {
          const result = await createQRCode(item)
          results.push(result)
        } catch (err) {
          errors.push({ input: item, error: (err as Error).message })
        }
      })
    )
  )

  return { results, errors }
}

Five concurrent requests is a reasonable default for PRO (100/min ≈ 1.67/sec). You could push to 10 concurrent and it'll still stay under the limit — but 5 gives you headroom for retries without blowing the budget. On BUSINESS (10,000/min), crank it to 50 or more.

Handling 409 slug conflicts and 429 rate limits

Two errors come up in bulk scenarios more than any other.

409 Conflict — your slug is already taken. The cleanest fix is to not specify a slug at all and let the API generate a unique one. If you need human-readable slugs (e.g. conf-2025-track-a-room-101), append a short timestamp or random suffix on conflict and retry. One retry is usually enough.

429 Too Many Requests — you've hit the rate limit. The response body includes a retryAfter value in seconds. Wait exactly that long, then retry. The function above handles this automatically. Don't implement exponential backoff on top of a server-provided retryAfter — just use the value you're given.

// 429 response shape
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded",
  "retryAfter": 12    // seconds until the window resets
}

// 409 response shape
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Slug 'conf-track-a' already exists"
}

Bulk operations on existing QR codes

Once codes exist, the API has five bulk modification endpoints. Each accepts up to 100 IDs per request — if you have more, paginate your calls.

POST/qr/bulk/pause
{ qrCodeIds: string[] }
Pause active codes (scans return 404)
POST/qr/bulk/resume
{ qrCodeIds: string[] }
Re-activate paused codes
POST/qr/bulk/delete
{ qrCodeIds: string[] }
Permanently delete codes (irreversible)
POST/qr/bulk/update-destination
{ qrCodeIds: string[], destinationUrl: string }
Swap destination URL for multiple codes at once
POST/qr/bulk/tags
{ qrCodeIds: string[], action: 'add'|'remove'|'replace', tags: string[] }
Manage tags in bulk — add, remove, or replace
// Example: bulk update-destination
const res = await fetch(`${API_BASE}/qr/bulk/update-destination`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    qrCodeIds: ['qr_abc123', 'qr_def456', 'qr_ghi789'],
    destinationUrl: 'https://example.com/new-campaign-page',
  }),
})

// Response: { updated: 3, failed: 0 }

Real-world example: 50 QR codes for a conference

A conference with 50 sessions needs 50 QR codes — one per session, pointing to that session's detail page. Sessions get assigned to rooms last-minute. And sometimes the session URL changes after the programme is printed. Here's how to handle all of that.

Step 1: Create all 50 codes in one script run.

const sessions = [
  { id: 'S01', title: 'Opening Keynote', track: 'main', url: 'https://conf.example.com/sessions/s01' },
  { id: 'S02', title: 'Redis Deep Dive',  track: 'engineering', url: 'https://conf.example.com/sessions/s02' },
  // ... 48 more
]

const inputs: QRCreateInput[] = sessions.map((s) => ({
  name: s.title,
  destinationUrl: s.url,
  slug: `conf-2025-${s.id.toLowerCase()}`,
  tags: ['conference-2025', `track:${s.track}`],
}))

const { results, errors } = await bulkCreateQRCodes(inputs, 5)
console.log(`Created: ${results.length}, Failed: ${errors.length}`)

Step 2: Tag by track for easy filtering later. Tags were already set on creation, but if you need to re-tag after the fact:

const engineeringIds = results
  .filter((r) => r.name.startsWith('Redis') || r.name.startsWith('Postgres'))
  .map((r) => r.id)

await fetch(`${API_BASE}/qr/bulk/tags`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    qrCodeIds: engineeringIds,
    action: 'add',
    tags: ['track:engineering'],
  }),
})

Step 3: Room assignments change — update destinations. The QR codes are already printed. No reprint needed — just point them somewhere new:

// Session S14 moved to the overflow room, new URL
await fetch(`${API_BASE}/qr/bulk/update-destination`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    qrCodeIds: ['qr_s14_id'],
    destinationUrl: 'https://conf.example.com/sessions/s14?room=overflow-b',
  }),
})

This propagates within 5 minutes (the cache TTL). If you need it live faster, delete the Redis key directly — though that's a platform-level operation, not something the API exposes publicly.

Exporting results to CSV

After a bulk creation run, you'll want a CSV mapping session IDs to short URLs — something you can share with the event team or import into a spreadsheet. The creation results have everything you need, and individual scan stats are available at GET /qr/:id/stats/export.

import { writeFileSync } from 'fs'

// Build CSV from creation results
const header = 'session_id,name,slug,short_url,qr_image_url'
const rows = results.map((r, i) => {
  const session = sessions[i]
  return [
    session.id,
    `"${r.name}"`,
    r.slug,
    r.shortUrl,
    r.qrCodeUrl,
  ].join(',')
})

writeFileSync('conference-qr-codes.csv', [header, ...rows].join('\n'))
console.log(`Saved ${rows.length} rows to conference-qr-codes.csv`)

// Fetch scan stats per code after the event
async function exportScanStats(qrId: string) {
  const res = await fetch(`${API_BASE}/qr/${qrId}/stats/export`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  })
  return res.json() // returns aggregated scan data
}

Plan limits for high-volume use

Before you start a bulk creation run, check that your plan's total QR code limit covers what you're building. The limits are per-account, not per-run:

PlanTotal QR codesCreates / minBulk ops (max IDs)
FREE510100
PRO50100100
BUSINESS25010,000100

The 100-ID cap on bulk operations is per-request, not per-day. If you need to pause 300 codes, send three requests of 100 each. The rate limits apply to bulk operation requests too, but at BUSINESS rates (10,000/min) this is not a practical constraint.

Common questions

Does the API have a bulk-create endpoint?

No, and that's deliberate. Create individually in parallel using a bounded concurrency pool. The error handling is cleaner and you get per-code success/failure semantics rather than a partial batch result you have to pick apart.

What are the rate limits?

FREE: 10 creates/min. PRO: 100 creates/min (~1.6/sec sustained). BUSINESS: 10,000 creates/min. On 429, the response includes a retryAfter value in seconds — use it.

Can I update all QR codes in a campaign to a new URL at once?

Yes — POST /qr/bulk/update-destination with up to 100 IDs and the new destinationUrl. Changes propagate within 5 minutes (cache TTL). If you have more than 100 codes, paginate into multiple requests.

I'm creating 500 codes for a product launch. Which plan do I need?

BUSINESS, which supports 250 total QR codes. If you need more than 250, get in touch — the Business plan limits can be extended for enterprise use cases.

Ready to automate QR code creation?

Full REST API on all plans. OpenAPI spec, bulk operations, and real-time scan analytics. Free plan to get started — no credit card.

High-volume or custom limits? Get in touch