Developers

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

base url
https://api.qroute.in/v1
RESTJSONOrg-scoped keysSigned webhooks

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
curl https://api.qroute.co/v1/qr-codes \
  -H "Authorization: Bearer qr_live_xxxxxxxxxxxxxxxx"

Scopes

  • qr:readRead QR codes and their status
  • qr:writeCreate, edit, pause, and archive codes
  • analytics:readRead aggregated scan analytics
  • domains:readRead custom domain state
  • exports: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.

response headers
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.

POST/v1/qr-codes
curl
curl -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

200 · json
{
  "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

GET/v1/qr-codes/{id}
curl
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.

PATCH/v1/qr-codes/{id}
curl
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.

GET/v1/qr-codes?project={id}&status=active&limit=20
200 · json
{
  "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.

POST/v1/qr-codes/{id}/pause
POST/v1/qr-codes/{id}/resume
POST/v1/qr-codes/{id}/archive

Analytics

Read privacy-safe aggregates by code, project, domain, or time window. Analytics are computed asynchronously and never sit on the redirect path.

GET/v1/analytics?qr={id}&from=2026-07-01&to=2026-07-27&group_by=day
200 · json
{
  "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.thresholdA code crosses a scan count you set
  • domain.state_changedA custom domain moves through its lifecycle
  • export.completedA CSV or print-pack export is ready
  • qr.status_changedA code is paused, resumed, or expires

Example payload

POST to your endpoint
{
  "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.

header
X-QRoute-Signature: t=1753632000,v1=5257a869e7b0...
node.js
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. 1. In Zapier/Make, create a “Catch Hook” trigger and copy its URL.
  2. 2. Add it as a webhook endpoint in API & Integrations and select events.
  3. 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.

StatusCodeMeaning
400invalid_requestMissing or malformed parameters
401unauthorizedMissing or invalid API key
403forbiddenKey lacks the required scope
404not_foundResource does not exist in this org
409conflictDuplicate or conflicting state
422unsafe_destinationDestination blocked by the allowlist
429rate_limitedToo many requests — back off
422 · json
{
  "error": {
    "code": "unsafe_destination",
    "message": "Destination uses a blocked protocol (javascript:)."
  }
}

Ready to build?

Create an API key and try the endpoints above.

Go to API & Integrations →