API & Integrations
Create and manage QR codes, read analytics, and receive real-time events. The API is REST over HTTPS, returns JSON, and is scoped to a single organization per key.
Base URL
https://api.qroute.in/v1
Authentication
Authenticate with a secret API key in the Authorization header. Create keys under API & Integrations. Keys are shown in full once and stored hashed. Never expose a secret key in client-side code.
curl https://api.qroute.co/v1/qr-codes \ -H "Authorization: Bearer qr_live_xxxxxxxxxxxxxxxx"
Scopes
qr:readRead QR codes and their statusqr:writeCreate, edit, pause, and archive codesanalytics:readRead aggregated scan analyticsdomains:readRead custom domain stateexports:writeTrigger CSV/print exports
Rate limits
Requests are limited per key. Every response includes limit headers — back off when X-RateLimit-Remaining hits zero and retry after the reset.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-RateLimit-Reset: 1753632000
Create a QR code
Create a dynamic (editable, tracked) or static code. Destinations are validated against a safe-protocol allowlist.
/v1/qr-codescurl -X POST https://api.qroute.co/v1/qr-codes \
-H "Authorization: Bearer qr_live_..." \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"mode": "dynamic",
"name": "Lesson 1 intro",
"destination": "https://learn.brand.com/lesson-1",
"project": "prj_sci_g6",
"domain": "scan.brand.com"
}'Response
{
"id": "q_lesson01",
"mode": "dynamic",
"status": "active",
"encoded_url": "https://scan.brand.com/q/lesson01",
"destination": "https://learn.brand.com/lesson-1",
"created_at": "2026-07-27T10:00:00Z"
}encoded_url is what the QR image contains — it never changes when you edit the destination.
Retrieve a code
/v1/qr-codes/{id}curl https://api.qroute.co/v1/qr-codes/q_lesson01 \ -H "Authorization: Bearer qr_live_..."
Edit destination
Update where a dynamic code points. The encoded QR image stays identical — printed codes keep working.
/v1/qr-codes/{id}curl -X PATCH https://api.qroute.co/v1/qr-codes/q_lesson01 \
-H "Authorization: Bearer qr_live_..." \
-d '{ "destination": "https://learn.brand.com/lesson-1-v2" }'List codes
Filter by project, status, or domain. Results are paginated with a cursor.
/v1/qr-codes?project={id}&status=active&limit=20{
"data": [ { "id": "q_lesson01", "status": "active", "scans": 4820 } ],
"has_more": true,
"next_cursor": "cur_8f2a"
}Pause & archive
Pause a code (scanners see a safe unavailable page) or archive it while preserving scan history.
/v1/qr-codes/{id}/pause/v1/qr-codes/{id}/resume/v1/qr-codes/{id}/archiveAnalytics
Read privacy-safe aggregates by code, project, domain, or time window. Analytics are computed asynchronously and never sit on the redirect path.
/v1/analytics?qr={id}&from=2026-07-01&to=2026-07-27&group_by=day{
"total_scans": 4820,
"unique_scans": 3910,
"series": [ { "date": "2026-07-26", "scans": 210 } ],
"top_countries": [ { "country": "IN", "scans": 2940 } ]
}Webhook events
Subscribe an endpoint to receive events. Deliveries are retried with exponential backoff.
scan.threshold— A code crosses a scan count you setdomain.state_changed— A custom domain moves through its lifecycleexport.completed— A CSV or print-pack export is readyqr.status_changed— A code is paused, resumed, or expires
Example payload
{
"id": "evt_9f2a",
"type": "scan.threshold",
"created_at": "2026-07-27T10:05:00Z",
"data": { "qr_id": "q_lesson01", "threshold": 5000, "scans": 5001 }
}Verify signatures
Every delivery is signed. Reject requests older than 5 minutes and compare a constant-time HMAC to prevent replay and forgery.
X-QRoute-Signature: t=1753632000,v1=5257a869e7b0...
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay guard
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Zapier & Make
No code required — point a Catch Hook at a webhook endpoint. Payloads are flat JSON with stable field names, so you can map fields directly into any downstream action.
- 1. In Zapier/Make, create a “Catch Hook” trigger and copy its URL.
- 2. Add it as a webhook endpoint in API & Integrations and select events.
- 3. Trigger a test event and map
data.*fields to your action.
Errors
The API uses conventional HTTP status codes and returns a machine-readable error body.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or malformed parameters |
| 401 | unauthorized | Missing or invalid API key |
| 403 | forbidden | Key lacks the required scope |
| 404 | not_found | Resource does not exist in this org |
| 409 | conflict | Duplicate or conflicting state |
| 422 | unsafe_destination | Destination blocked by the allowlist |
| 429 | rate_limited | Too many requests — back off |
{
"error": {
"code": "unsafe_destination",
"message": "Destination uses a blocked protocol (javascript:)."
}
}