Developers
QR code API and webhooks
Create and edit dynamic codes from your own systems, read their scan analytics, and hear about scans and form entries the moment they happen. JSON over HTTPS, on the Pro plan and above.
OpenAPI description for Postman, client generators and no-code tools.
Authentication
Create a key under Developers in the dashboard and send it as a bearer token. A key acts as the person who created it, with only the permissions it was given, and stops working if they leave the workspace. Keep keys on a server; a key in a web page or an app is a key anyone can use.
curl https://qr.div-systems.com/api/v1/codes \
-H "Authorization: Bearer qrb_live_…"| codes:read | List and read codes |
|---|---|
| codes:write | Create, edit, pause and archive dynamic codes |
| analytics:read | Read daily scan totals |
| webhooks:write | Subscribe and unsubscribe webhook endpoints |
Endpoints
Every path is relative to https://qr.div-systems.com/api/v1. Responses wrap the result in data.
| Request | Does | Needs |
|---|---|---|
| GET /codes | List codes, newest first | codes:read |
| POST /codes | Create a dynamic URL code | codes:write |
| GET /codes/{id} | Get one code | codes:read |
| PATCH /codes/{id} | Change name, destination or status | codes:write |
| DELETE /codes/{id} | Archive a code | codes:write |
| GET /codes/{id}/analytics | Daily scan totals | analytics:read |
| GET /codes/{id}/image | The code as a verified SVG or PNG | codes:read |
| GET /webhooks | List webhook endpoints | webhooks:write |
| POST /webhooks | Subscribe an endpoint | webhooks:write |
| DELETE /webhooks/{id} | Unsubscribe an endpoint | webhooks:write |
Create a code
curl -X POST https://qr.div-systems.com/api/v1/codes \
-H "Authorization: Bearer qrb_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"Table 7","destination":"https://example.com/menu"}'The response includes the short link's slug. Change the destination at any time with PATCH; every printed copy follows. Fetch /codes/{id}/image for a file: it is decoded before it is returned, like every download from QRBrand.
Pages of results
Lists return up to limit items, newest first, and a next_before timestamp. Pass it as before for the next page; it is null on the last one.
Errors and limits
Errors have a stable code to branch on and a message written for a person.
HTTP/1.1 409 Conflict
{ "error": { "code": "plan_limit", "message": "Your plan includes 500 dynamic codes and all of them are in use. …" } }401 means the key is missing, unknown, revoked or expired; 403 that it lacks the permission or the plan no longer includes the API; 404 that nothing with that id is in your workspace; 409 that the plan is full; 422 that the request was understood but not acceptable. Each key may make 120 requests a minute; past that the answer is 429 with a Retry-After header. A code frozen because a plan lapsed still redirects, but cannot be edited until the plan is renewed.
Webhooks
Add an https endpoint under Developers, or subscribe one with POST /webhooks. We send a JSON POST for each event you choose. Deliveries are retried for about a day, and every attempt is listed in the dashboard, where any delivery can be sent again.
| code.created | A code was created |
|---|---|
| code.updated | A code’s name, destination or status changed |
| code.archived | A code was archived |
| code.scanned | A person scanned a code (crawlers and link previews excluded) |
| form.submitted | Someone filled in a form on a landing page |
| bulk.completed | A bulk upload finished |
{
"id": "evt_4f0c…",
"type": "code.scanned",
"created_at": "2026-09-18T12:00:00Z",
"data": { "code_id": "…", "scanned_at": "…", "country": "GB", "device": "mobile", "os": "iOS" }
}Delivery is at least once: use id to ignore a repeat. Scan events never include anything that identifies the person scanning.
Checking a delivery came from us
Each request carries a QRBrand-Signature header: t=<unix time>,v1=<HMAC-SHA256>, computed over the timestamp, a full stop and the raw body, with your endpoint's signing secret. Refuse anything that does not match or is more than five minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto';
// body must be the raw request body, before any JSON parsing.
export function isFromQRBrand(header, body, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const timestamp = Number(parts.t);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // older than 5 minutes
const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
return parts.v1?.length === expected.length &&
timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}