Errors
Every status code, every error message, and what to do about each one.
Errors
Every error response uses a standard HTTP status code and a JSON body.
Status code reference
| Code | Meaning | Caused by |
|---|---|---|
200 OK | Success | n/a |
201 Created | Resource created | Successful POST to /keys/, /dynamic/, /webhooks/ |
204 No Content | Resource deleted | Successful DELETE on /keys/{id}/, /dynamic/{short_id}/, /webhooks/{id}/ |
302 Found | Redirect | The public scan endpoint /q/{short_id}/ |
400 Bad Request | Validation failed | Bad params, invalid hex, SSRF refusal, plan-cap on bulk size |
401 Unauthorized | Auth missing or invalid | No X-Api-Key or no valid JWT, revoked key |
402 Payment Required | Locked behind upgrade | Free tier exceeded its 1 lifetime destination edit |
403 Forbidden | Plan gate hit | Asked beyond your plan's caps |
404 Not Found | Resource missing | DELETE / GET on a non-existent key, dynamic, or webhook |
409 Conflict | Idempotency mismatch | Same Idempotency-Key reused with different body |
422 Unprocessable Entity | Per-item validation | Bulk endpoint with at least one invalid item |
426 Upgrade Required | HTTP not allowed | Plain HTTP on /api/* |
429 Too Many Requests | Quota or rate limit | Free / Enterprise hit monthly cap, or rate limit triggered |
500 Internal Server Error | Render or unexpected failure | Should be rare. Retry once. |
503 Service Unavailable | Maintenance or overload | Status page tells you which |
Body shape
For top-level errors:
{ "detail": "Plan 'free' allows up to 3\". Upgrade for larger sizes." }For per-field validation errors:
{
"size_inches": ["Ensure this value is less than or equal to 15."],
"color": "Invalid color 'rouge'. Use 'black', 'white', or hex like #RRGGBB."
}For bulk per-item errors:
{
"errors": {
"3": "size_inches > plan max (8)",
"17": "Invalid color 'rouge'."
},
"rendered_before_failure": 17
}Common errors and their fixes
400: "data": ["This field is required."]
You sent data_type: "url" but no data. Add the URL string.
400: "size_inches": ["Ensure this value is less than or equal to 15.0"]
The hard cap is 15 inches. If you need bigger, render at 15 inches and tile or scale in your printing software.
400: "logo_url": "refusing to fetch ...: resolves to private/internal IP ..."
SSRF protection blocked the URL because it points to an internal network. Host the logo on a public CDN.
400: "logo_file": "Max 2097152 bytes."
Logo is over 2 MB. Optimize it (tinypng.com, imagemin) or scale it
down before uploading.
401: "Invalid or revoked API key."
The X-Api-Key header is missing, malformed, or points to a revoked
key. Verify it starts with smk_ and is the current value (not a
rotated-out copy).
402: Free tier destination edit cap
{
"detail": "Plan 'free' allows 1 destination edit per dynamic QR. This QR has been edited 1 time(s) already. Upgrade to Starter ($7/mo) or higher for unlimited edits, or buy a credit pack."
}Upgrade to any paid plan or buy credits.
403: Size or DPI cap
{ "detail": "Plan 'free' allows up to 3\". Upgrade for larger sizes." }{ "detail": "Plan 'starter' caps DPI at 300. Upgrade for higher resolution." }Reduce the value or upgrade.
403: Custom logo not allowed
{ "detail": "Plan 'free' doesn't support custom logos." }Upgrade to Starter or higher.
403: Bulk endpoint disabled
{ "detail": "Plan 'free' doesn't include bulk generation. Upgrade to Starter or higher." }422: Bulk per-item failure
{
"errors": {
"3": "size_inches > plan max (8)",
"17": "Invalid color 'rouge'."
},
"rendered_before_failure": 17
}Fix every offending item and retry the whole batch.
429: Monthly quota exhausted
{ "detail": "Monthly quota of 5 exceeded for plan 'free'." }Wait until the 1st of next month, upgrade, or buy a credit pack.
429: Rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 12Throttle your client to your plan's documented req/min and re-try
after Retry-After seconds.
500: Internal error
{ "detail": "Render engine failure. Retry once or contact support." }Should be rare. Retry the exact same request once. If it persists, file a support ticket including:
- The full request body
- The response status code and body
- The
X-Request-Idvalue from the response headers
Handling errors in code
Python
import requests
r = requests.post(
"https://api.qrstudio.agency/api/v1/generate/",
headers={"X-Api-Key": "smk_..."},
json={"data": "https://x.com", "size_inches": 4},
)
if r.status_code == 429:
showUpgradeUI()
elif r.status_code == 403:
log.warning("Plan gate: %s", r.json()["detail"])
elif r.status_code == 401:
alertOps("Bad API key")
elif not r.ok:
log.error("API error %s: %s", r.status_code, r.text)
else:
with open("qr.png", "wb") as f:
f.write(r.content)JavaScript
const r = await fetch("/api/v1/generate/", );
if (r.status === 429) {
showUpgradeModal();
} else if (r.status === 403) {
const { detail } = await r.json();
console.warn("Plan gate:", detail);
} else if (r.status === 401) {
console.error("Bad API key");
} else if (!r.ok) {
console.error("Server error", r.status);
} else {
const blob = await r.blob();
// ...
}Retry policy recommendation
| Status | Retry? | Why |
|---|---|---|
200 | n/a | You got the result |
400 to 403 | No | Caller bug, retry will not help |
404 | No | Resource truly missing |
409 | No | Idempotency mismatch, resolve client-side |
422 | No | Per-item validation failure on bulk |
429 | After Retry-After | Re-running before that does nothing |
5xx | Yes, with backoff | Likely transient |
Recommended backoff for 5xx: exponential, base 1 second, max 3 retries, add jitter so concurrent clients do not stampede.