Developer documentation

QRrd Business API

Business accounts get API access through their own dedicated QRrd hostname and Worker.

Base URL
https://<tenant>.qrrd.cc/v1
1 credit = 1 successful API request
View QRrd Business

Quick start

Open the Developer page in your QRrd Business account, create an API key and copy the secret when it is shown. API key secrets begin with qrrd_live_ and are shown only once.

List QR codes
curl https://YOUR-TENANT.qrrd.cc/v1/qr-codes \
  -H "Authorization: Bearer qrrd_live_YOUR_KEY"

Authentication and scopes

Every /v1 request requires an API key in the HTTP Authorization header. Browser session cookies do not provide a backdoor to the public API.

Authorization header
Authorization: Bearer qrrd_live_YOUR_KEY
ScopeAllows
qr:readList and retrieve QR records.
qr:createCreate QR records.
qr:updateUpdate existing QR records.
analytics:readRead QR scan analytics.
Keep API keys server-side. Do not embed a qrrd_live_ key in browser JavaScript, an HTML email or a public mobile-app bundle.

API credits

One credit is used for each successful authenticated request to the QRrd /v1 API. A successful API request keeps one reserved API credit. Authentication failures, insufficient scopes, validation errors and server failures do not keep that credit.

Creating a QR through the API uses one API credit and one QR credit: the API credit pays for the successful request and the QR credit issues the permanent public QR token.

When the API-credit balance is exhausted, QRrd returns HTTP 402. API credit allocation is managed with the Business account.

Endpoints

MethodPathScopePurpose
GET/v1/qr-codesqr:readList QR codes.
POST/v1/qr-codesqr:createCreate a QR code.
GET/v1/qr-codes/:idqr:readRetrieve one QR code.
PATCH/v1/qr-codes/:idqr:updateUpdate one QR code.
GET/v1/qr-codes/:id/analyticsanalytics:readRead recent scans and daily totals.

The list endpoint accepts ?limit= from 1 to 250 and returns newest-created records first. API retirement/deletion is not currently exposed through /v1.

Create a QR code

POST /v1/qr-codes accepts JSON. name is required. Set destination_type to redirect, message or unassigned.

Create a redirect QR
curl https://YOUR-TENANT.qrrd.cc/v1/qr-codes \
  -X POST \
  -H "Authorization: Bearer qrrd_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Smith proposal",
    "recipient_name": "John Smith",
    "recipient_company": "Example Industries",
    "reference": "CRM-10291",
    "destination_type": "redirect",
    "destination_url": "https://example.com/proposals/john-smith",
    "alert_mode": "first"
  }'

Destination fields

FieldWhen usedNotes
nameAlwaysRequired, maximum 120 characters.
pool_idOptionalUses the workspace default pool when omitted.
recipient_name, recipient_company, referenceOptionalYour own assignment/CRM metadata.
destination_typeRecommendedredirect, message or unassigned.
destination_urlRedirectRequired for redirect QRs. Must be HTTP or HTTPS.
message_title, message_bodyMessageBoth required for hosted-message QRs.
message_button_text, message_button_urlMessageOptional call-to-action.
message_passwordMessageOptional, 6–128 characters. The password is not returned by the API.
alert_modeOptionaloff, first or every. Enabling alerts requires alert credits.
statusOptionalactive or disabled.

A successful response has HTTP 201. The returned short_url is the scannable destination and image_url is the public 512px PNG suitable for HTML email and mail merge.

Example response
{
  "data": {
    "id": "qr_...",
    "code": "7Km3Qp9X2R",
    "short_url": "https://YOUR-TENANT.qrrd.cc/q/7Km3Qp9X2R",
    "image_url": "https://YOUR-TENANT.qrrd.cc/img/7Km3Qp9X2R.png",
    "name": "John Smith proposal",
    "destination_type": "redirect",
    "destination_url": "https://example.com/proposals/john-smith",
    "alert_mode": "first",
    "status": "active",
    "scan_count": 0
  },
  "webhook_secret": null
}

Update a QR code

PATCH /v1/qr-codes/:id is a partial update. Fields you omit retain their existing values.

Change a redirect destination
curl https://YOUR-TENANT.qrrd.cc/v1/qr-codes/qr_YOUR_ID \
  -X PATCH \
  -H "Authorization: Bearer qrrd_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/proposals/john-smith-v2"
  }'

The public QR URL and public code do not change when you update its destination.

Read scan analytics

GET /v1/qr-codes/:id/analytics returns the QR record, up to the 100 most recent scans and up to 90 days of daily scan totals.

Analytics request
curl https://YOUR-TENANT.qrrd.cc/v1/qr-codes/qr_YOUR_ID/analytics \
  -H "Authorization: Bearer qrrd_live_YOUR_KEY"
Analytics shape
{
  "data": {
    "qr": { "id": "qr_...", "code": "7Km3Qp9X2R", "scan_count": 3 },
    "recent_scans": [
      {
        "event_id": "evt_...",
        "scanned_at": "2026-08-19T01:23:45.000Z",
        "country": "AU",
        "region": "South Australia",
        "city": "Adelaide",
        "device_type": "mobile",
        "browser": "Chrome"
      }
    ],
    "daily": [
      { "day": "2026-08-19", "scan_count": 3 }
    ]
  }
}

Location and device values are approximate and may be unavailable. QRrd does not expose a raw scanner IP address in scan analytics.

Hosted QR images

Every QR response includes a public 512px PNG URL:

Public PNG
https://YOUR-TENANT.qrrd.cc/img/7Km3Qp9X2R.png

It can be merged into HTML email without using an API credit:

HTML email
<img src="https://YOUR-TENANT.qrrd.cc/img/7Km3Qp9X2R.png"
     alt="QR code"
     width="180"
     height="180">
An image load is not a scan. Requests to /img/:code.png do not increment scan analytics, consume API credits, send alerts or fire webhooks. Only a visit to the QR's /q/:code URL records a scan.

Scan webhooks

Business Owners/Managers configure HTTPS webhook endpoints from the Developer page. When a configured QR is scanned, QRrd sends a JSON POST and signs the exact request body with that QR's signing secret.

HeaderValue
qrrd-eventqr.scan
qrrd-deliveryUnique scan event/delivery ID. Use this for idempotency.
qrrd-timestampUnix timestamp used in the signature.
qrrd-signaturesha256=<hex HMAC>
Webhook payload
{
  "id": "evt_...",
  "type": "qr.scan",
  "created_at": "2026-08-19T01:23:45.000Z",
  "data": {
    "workspace_id": "wsp_...",
    "qr": {
      "id": "qr_...",
      "code": "7Km3Qp9X2R",
      "name": "John Smith proposal",
      "recipient_name": "John Smith",
      "recipient_company": "Example Industries",
      "reference": "CRM-10291",
      "scan_count": 1
    },
    "scan": {
      "timestamp": "2026-08-19T01:23:45.000Z",
      "first_scan": true,
      "country": "AU",
      "region": "South Australia",
      "city": "Adelaide",
      "device_type": "mobile",
      "browser": "Chrome"
    }
  }
}

Verify the signature

Compute HMAC-SHA256 over qrrd-timestamp + "." + rawRequestBody with the webhook signing secret, then compare it to the hexadecimal value after sha256=. Verify against the raw body before parsing JSON.

Node.js verification example
import crypto from "node:crypto";

function verifyQrrdWebhook({ rawBody, timestamp, signature, secret }) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const received = String(signature || "").replace(/^sha256=/, "");
  if (expected.length !== received.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex")
  );
}

Webhook delivery can be retried, so consumers should treat qrrd-delivery as an idempotency key. It is also good practice to reject timestamps that are unexpectedly old for your application.

Errors

Errors are JSON and include a stable machine-readable error code plus a human-readable message.

Error shape
{
  "error": "insufficient_scope",
  "message": "API key does not have analytics:read"
}
StatusTypical meaning
400Invalid field or request data.
401Missing or invalid API key.
402Workspace/API/QR/alert credits required for the requested operation.
403API key lacks the required scope or workspace permission.
404QR record or endpoint not found.
415A JSON request was sent without Content-Type: application/json.
500Unexpected server failure. No API credit is kept for the failed request.

Ready to integrate?

Create an account, fund the workspace with a credit pack, then generate your first API key from the Developer page.