Heldqr API.
Build on top.
The Heldqr Management API gives Business-tier accounts programmatic access to create, edit, and read scan analytics on QR codes. Bearer-token auth, JSON in, JSON out, idempotent writes.
Authentication.
Every request carries an Authorization header with a bearer token. One token belongs to one account; what it can do is what that account's plan allows. There are no scoped or read-only tokens today — we'll add them when there's a real second persona inside an account to scope to.
Authorization: Bearer hkr_live_a3f...
Tokens are issued manually right now. Email api@heldqr.com from the address on your account and we'll provision one. A self-service issuance UI is tracked under heldqr-8uny — until that ships, the manual loop is the path, and a Heldqr employee answers within one working day.
Requests without a valid token return 401:
HTTP/2 401
Content-Type: application/json
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid bearer token."
}
}
Examples on this page use api.heldqr.com as the host. The dedicated subdomain ships with heldqr-a5y8 / heldqr-mhlc; until then the same routes also answer on app.heldqr.com — substitute the hostname if you want to run examples before the move.
Rate limits.
Every authenticated request spends one credit against a sliding-window counter — 600 requests per minute per token. The window decays continuously rather than resetting on the wall-clock minute, so a burst of 50 requests in the first second leaves you with 550 over the rest of the window, not zero.
Your remaining budget is on every response:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 593
X-RateLimit-Reset: 1731764400
Over the cap, the response is 429 with a Retry-After header telling you how many seconds to wait:
HTTP/2 429
Retry-After: 7
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
Content-Type: application/json
{"error":{"code":"rate_limited","message":"Too many requests."}}
The fair-use policy at heldqr.com/fair-use is the canonical source for these numbers. If they ever change, that page changes first, and the changelog here gets an entry.
Idempotent writes.
Network requests fail in surprising ways: a TCP connection that resets after the server processed the body, a proxy that retries without telling you, your own HTTP client deciding to retry on a timeout. For the one endpoint that creates state — POST /codes — pass an Idempotency-Key header so a retry doesn't create a duplicate code.
POST /api/v1/codes
Authorization: Bearer $TOKEN
Content-Type: application/json
Idempotency-Key: 5e3d40a2-9b87-4c12-a3e3-6f8d1f9c2b0a
{"target_url": "https://example.com/menu"}
The key is yours to generate — a UUIDv4 is plenty. Anything 1-255 bytes works. We store the (token, key) pair with the response body for 24 hours.
Retry within the window with the same body and you get the original response back, plus an Idempotent-Replay: true header so your client knows it didn't create a duplicate:
HTTP/2 201
Idempotent-Replay: true
Content-Type: application/json
{"data": { ... same response as the first call ... }}
Retry with the same key but a different body — different target_url, say — and we return 409, because keys aren't supposed to mean 'ignore my new request':
HTTP/2 409
Content-Type: application/json
{
"error": {
"code": "idempotency_key_conflict",
"message": "Idempotency-Key was reused with a different request body."
}
}
Keys are scoped per token. Two different tokens using the same key value don't collide. The header is optional but recommended — without it, a network retry can land two codes on the same target.
Error envelope.
Every error response shares the same shape. There is one and only one error object per response — never an array, never a top-level message field.
{
"error": {
"code": "snake_case_identifier",
"message": "Human-readable description.",
"field_errors": {"target_url": ["is invalid"]}
}
}
error.code is the stable identifier you should switch on. error.message is human-readable — fine to surface to your users, but don't pattern-match on it; we may rewrite messages for clarity without changing codes. field_errors only appears on 422 responses where a request body fails validation; the keys match the input field names.
Codes you'll see most often:
The full enum is in the OpenAPI spec at /reference. Treat any unfamiliar error.code as retryable on the same idempotency key — we may add new codes without bumping the API version, but we won't repurpose existing ones.
curl examples.
Set the token in your shell so the snippets below stay readable:
export HELDQR_TOKEN=hkr_live_a3f...
List your codes
curl https://api.heldqr.com/api/v1/codes \
-H "Authorization: Bearer $HELDQR_TOKEN"
{
"data": [
{
"shortcode": "menu-v3",
"target_url": "https://example.com/menu",
"label": "Restaurant menu",
"status": "active",
"created_at": "2026-05-15T14:21:03Z"
}
],
"pagination": {"page": 1, "page_size": 50, "total": 1, "total_pages": 1}
}
Create a code (with idempotency)
curl -X POST https://api.heldqr.com/api/v1/codes \
-H "Authorization: Bearer $HELDQR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"target_url": "https://example.com/menu", "label": "Restaurant menu"}'
Returns 201 with the created code. If you omit shortcode, one is generated; if you set it, we validate that it doesn't collide with an existing code on any account.
Update a code's destination
curl -X PATCH https://api.heldqr.com/api/v1/codes/menu-v3 \
-H "Authorization: Bearer $HELDQR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_url": "https://example.com/menu-2026"}'
The printed QR keeps working; new scans land on the new target within 60 seconds. The shortcode is permanent and can't change — that's the whole point of dynamic codes.
Read scan rollups
curl https://api.heldqr.com/api/v1/codes/menu-v3/scans \
-H "Authorization: Bearer $HELDQR_TOKEN"
The shape depends on the token's account plan: Free sees lifetime totals only, Pro adds 30-day daily buckets with country + device breakdowns, Business adds a 1-year window. Full response schemas are in the OpenAPI spec at /reference.