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-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.
// 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:
| Plan | Total QR codes | Creates / min | Bulk ops (max IDs) |
|---|---|---|---|
| FREE | 5 | 10 | 100 |
| PRO | 50 | 100 | 100 |
| BUSINESS | 250 | 10,000 | 100 |
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