Error format
All errors follow a consistent JSON structure:{
"error": {
"code": "not_found",
"message": "QR code not found",
"doc_url": "https://docs.useqrkit.com/errors#not_found",
"request_id": "req_abc123"
}
}
| Field | Description |
|---|---|
code | Machine-readable error code |
message | Human-readable description |
doc_url | Link to relevant documentation |
request_id | Unique ID for debugging (include this when contacting support) |
Error codes
| HTTP Status | Code | Description |
|---|---|---|
400 | invalid_request | The request body is malformed or missing required fields |
401 | unauthorized | Missing or invalid API key — including a revoked or expired key, and a key whose workspace has been deleted |
403 | forbidden | The API key does not have the required scope, your plan does not include this feature, or the workspace’s billing is paused (example below) |
404 | not_found | The requested resource does not exist |
409 | conflict | The request collides with the current state of a resource — e.g. a GS1 identifier already registered, or restoring a code that is not in the Trash. Check reason (below) |
409 | target_not_editable | PATCH sent target for a code that holds a page rather than a link (link or review page, form, menu, app-store, smart-URL or uploaded-file code). Nothing in the request was applied; edit its content in the dashboard |
422 | unprocessable_entity | The request is valid but cannot be processed (e.g., plan limit reached — reason: PLAN_LIMIT) |
422 | destination_unsafe | The destination is on QRKit’s blocklist or flagged by Google Web Risk as phishing or malware. Use a different destination; retrying the same URL fails the same way. |
429 | rate_limit_exceeded | Too many requests — see Rate Limiting |
500 | internal_error | An unexpected error occurred on our end |
reason
Some errors carry a machine-readable reason next to code, for cases the HTTP status alone does not tell apart:
reason | Status | When |
|---|---|---|
NOT_IN_TRASH | 409 | Restore of a code that is live (is_active: true). Nothing to restore — if you retried after a lost response, treat it as done |
DEACTIVATED | 409 | Restore of a code that was switched off (is_active: false) but never deleted, so it is not in the Trash. Not a success: its redirect is still off. Re-activate it with PATCH and is_active: true |
DOMAIN_RETIRED | 409 | Restoring, or PATCH with is_active: true on, a GS1 code whose custom domain was removed — the code can never resolve again |
PLAN_LIMIT | 422 | Creating a dynamic code, restoring one, or turning one back on with PATCH is_active: true, when the plan has no free QR code slot (GS1 codes: no free GS1 slot). Also carries limit, current_count and plan_id. Enforced per workspace even for parallel requests and batches. In a batch, an item that ran out of room has status: error and reason: PLAN_LIMIT in its entry under items |
Example responses
400 — Invalid request
{
"error": {
"code": "invalid_request",
"message": "target: target.destination is required for url QR codes",
"doc_url": "https://docs.useqrkit.com/errors#invalid_request",
"request_id": "req_x1y2z3"
}
}
401 — Unauthorized
{
"error": {
"code": "unauthorized",
"message": "Invalid API key",
"doc_url": "https://docs.useqrkit.com/errors#unauthorized",
"request_id": "req_a1b2c3"
}
}
403 — Billing paused
While a workspace’s billing is paused, every API call with its keys is refused until billing restarts. The workspace owner can resume billing earlier from the QRKit dashboard. Existing QR codes keep redirecting and scanning during the pause.{
"error": {
"code": "forbidden",
"message": "Billing for this workspace is paused until 2026-12-01. API access resumes when billing restarts; the workspace owner can resume billing earlier from the QRKit dashboard.",
"doc_url": "https://docs.useqrkit.com/errors#forbidden",
"request_id": "req_p4q5r6"
}
}
404 — Not found
{
"error": {
"code": "not_found",
"message": "QR code 'qr_999' not found",
"doc_url": "https://docs.useqrkit.com/errors#not_found",
"request_id": "req_d4e5f6"
}
}
422 — Plan limit reached
{
"error": {
"code": "unprocessable_entity",
"message": "QR code limit reached for your plan. Upgrade to create more.",
"doc_url": "https://docs.useqrkit.com/errors#unprocessable_entity",
"request_id": "req_g7h8i9"
}
}
422 — Restore refused by the plan limit
{
"error": {
"code": "unprocessable_entity",
"message": "QR code limit reached for your starter plan (12 QR codes). Delete or deactivate a dynamic code, or upgrade, to restore this one.",
"reason": "PLAN_LIMIT",
"limit": 12,
"current_count": 12,
"plan_id": "starter",
"doc_url": "https://docs.useqrkit.com/errors#unprocessable_entity",
"request_id": "req_j1k2l3"
}
}
409 — Restore of a switched-off code
{
"error": {
"code": "conflict",
"message": "QR code is not in the Trash: it was deactivated, not deleted, and its redirect is still off. Re-activate it with PATCH /v1/qr-codes/{id} and is_active: true.",
"reason": "DEACTIVATED",
"doc_url": "https://docs.useqrkit.com/errors#conflict",
"request_id": "req_s7t8u9"
}
}
429 — Rate limited
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 30 seconds.",
"doc_url": "https://docs.useqrkit.com/errors#rate_limit_exceeded",
"request_id": "req_j1k2l3"
}
}
Handling errors
import requests
response = requests.post(
"https://api.useqrkit.com/v1/qr-codes",
headers={"Authorization": "Bearer qr_live_..."},
json={"target": {"type": "url", "destination": "https://example.com"}},
)
if not response.ok:
error = response.json()["error"]
print(f"Error {response.status_code}: {error['code']} — {error['message']}")
print(f"Request ID: {error['request_id']}")
const response = await fetch("https://api.useqrkit.com/v1/qr-codes", {
method: "POST",
headers: {
Authorization: "Bearer qr_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
target: { type: "url", destination: "https://example.com" },
}),
});
if (!response.ok) {
const { error } = await response.json();
console.error(`Error ${response.status}: ${error.code} — ${error.message}`);
console.error(`Request ID: ${error.request_id}`);
}

