Resources
Errors and troubleshooting
Every status and error code the API returns, what causes it, how to fix it and whether it is worth retrying. If something is failing, start with the troubleshooting table.
The error object
A failed request answers with a 4xx or 5xx status and a JSON body in one shape:
{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": [
{
"field": "fields.links.0.type",
"message": "Invalid enum value",
"code": "invalid_enum_value"
},
{
"field": "shortCode",
"message": "Slugs can only contain letters, numbers, and hyphens",
"code": "invalid_string"
}
]
}
}| Field | What it is | Use it for |
|---|---|---|
error.type | The broad category, such as invalid_request or permission_error. | Grouping. |
error.code | A stable, specific identifier. | Branching in code. Match on this. |
error.message | A sentence for a person. The wording can change. | Logs and error screens. Do not match on it. |
error.details | Present on some errors: a list of the individual problems. | Showing which field is wrong. |
Three shapes of details
| When | Each entry looks like |
|---|---|
| A request body failed validation | { "field": "fields.links.0.type", "message": "…", "code": "invalid_enum_value" } |
| A query parameter failed validation | { "path": ["limit"], "message": "…", "code": "too_big" }. Note path is an array here, not a dotted field. |
| A template rule was broken | { "key": "company", "reason": "locked" }, where reason is locked, hidden or required. |
Every problem is reported at once, so one round trip is enough to find them all.
Status codes at a glance
| Status | Name | In this API it means |
|---|---|---|
400 | Bad request | The request cannot be used as sent: a taken or reserved slug, an over-long idempotency key, or a malformed campaign body. |
401 | Unauthorized | No valid API key was sent. |
403 | Forbidden | The key is valid but not allowed: a missing scope, a plan without the REST API, a plan limit reached, or a campaign-scoped key out of bounds. |
404 | Not found | The id does not exist in this account, or the path is not part of the API. |
409 | Conflict | The request collides with something that exists: a duplicate identifier in a campaign, or an idempotency key reused or still in flight. |
422 | Unprocessable | The body or query is well-formed but a value is not valid, or the card’s template does not allow it. |
429 | Too many requests | The key’s per-minute limit was passed. |
500 | Server error | A fault on QRBold’s side. Also returned for malformed JSON on two endpoints; see the exceptions below. |
Success is 200 for reads and updates, 201 for creation and 204, with no body, for deletion.
Error codes
30 entries, in order of status. Each has an anchor, so you can link straight to one.
| Status | Code | Meaning |
|---|---|---|
401 | invalid_api_key | The request did not carry a key the API accepts. |
403 | forbidden | The key is valid but does not hold the scope this endpoint needs. |
403 | upgrade_required_api | The account’s plan does not include the REST API. |
403 | key_scoped_to_campaign | A campaign-scoped key was used somewhere it is not allowed. |
403 | limit_exhausted_cards | The account has used every card its plan covers. |
403 | limit_exhausted_products | The account has reached its plan’s QR code limit. |
403 | campaign_api_disabled | The campaign is not accepting API requests. |
404 | resource_not_found | Nothing with that id exists in this account. |
404 | unknown_endpoint | The path or method is not part of the API. |
422 | validation_failed | The request body or the query string did not pass validation. |
422 | validation_failed | The card’s template does not allow what was sent. |
400 | invalid_request | The custom shortCode is already taken. |
400 | reserved_short_link | The custom shortCode is a word the platform keeps for itself. |
409 | already_exists | A record with a value that must be unique already exists. |
409 | idempotency_key_reused | This Idempotency-Key was already used with a different request. |
409 | idempotency_key_in_flight | The first request with this Idempotency-Key is still running. |
400 | invalid_idempotency_key | The Idempotency-Key header is too long. |
409 | api_not_configured | The campaign has never had an API configuration activated. |
409 | duplicate_external_id | A card with that identifier already exists in the campaign. |
409 | duplicate_in_trash | A card with that identifier is sitting in the account’s Trash. |
409 | short_link_already_exists | The custom short link the campaign resolved from the payload is taken. |
422 | missing_external_id | The payload did not carry your identifier for the person. |
422 | invalid_external_id | The identifier is not usable. |
422 | invalid_short_link | The custom short link in a campaign payload is not valid. |
422 | invalid_payload | A native-mode campaign payload does not match the template’s schema. |
400 | invalid_json | The request body is not valid JSON. |
400 | invalid_json_structure | The body is valid JSON but not an object. |
400 | payload_too_large | The request body is over the size limit. |
429 | (no code) | The key has used its requests for this minute. |
500 | internal_error | Something failed on QRBold’s side. |
401 invalid_api_key
- Meaning
- The request did not carry a key the API accepts.
- Likely cause
- The header is missing, the key is mistyped, revoked, expired, or the account was closed. The response is deliberately the same for all of them.
- How to fix it
- Send Authorization: Bearer followed by the full key. If it still fails, create a new key in the dashboard.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Invalid API key"
}
}403 forbidden
- Meaning
- The key is valid but does not hold the scope this endpoint needs.
- Likely cause
- The key was created without that permission. Read is not implied by write.
- How to fix it
- Create a new key with the scope named in the message. Scopes cannot be added to an existing key.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "forbidden",
"message": "This API key is missing the \"cards:write\" scope. Create a new key with that scope to call this endpoint."
}
}403 upgrade_required_api
- Meaning
- The account’s plan does not include the REST API.
- Likely cause
- The key is genuine, but REST access needs the Pro plan or the Business card plan. It is checked on every request, so a downgrade stops working keys.
- How to fix it
- Upgrade the account, or use the key with an AI assistant, which every plan includes.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "upgrade_required_api",
"message": "API access is available on the Pro plan. Your Launch plan does not include it."
}
}403 key_scoped_to_campaign
- Meaning
- A campaign-scoped key was used somewhere it is not allowed.
- Likely cause
- The key is restricted to one campaign and was used on an account-wide endpoint, or on a different campaign.
- How to fix it
- Use an account-wide key for /cards, /templates, /qr-codes and /analytics, or call the campaign the key belongs to.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "key_scoped_to_campaign",
"message": "This API key is restricted to a single campaign and cannot be used on account-wide endpoints."
}
}403 limit_exhausted_cards
- Meaning
- The account has used every card its plan covers.
- Likely cause
- One seat is one card. The card was not created.
- How to fix it
- Add cards to the plan in the dashboard, or delete cards that are no longer needed.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "limit_exhausted_cards",
"message": "Your Team plan covers 5 cards. Add cards to your plan to create more."
}
}403 limit_exhausted_products
- Meaning
- The account has reached its plan’s QR code limit.
- Likely cause
- The plan allows a fixed number of live QR codes. The code was not created.
- How to fix it
- Upgrade the plan, or archive or delete codes that are no longer needed.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "limit_exhausted_products",
"message": "Your Pro plan allows a maximum of 1000 QR codes. Upgrade to create more."
}
}403 campaign_api_disabled
- Meaning
- The campaign is not accepting API requests.
- Likely cause
- The campaign is paused, archived or still a draft, or its API switch is off.
- How to fix it
- Set the campaign to active and switch its API on, on the campaign’s API tab in the dashboard.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "permission_error",
"code": "campaign_api_disabled",
"message": "The API is turned off for this campaign."
}
}404 resource_not_found
- Meaning
- Nothing with that id exists in this account.
- Likely cause
- The id is wrong, belongs to another account, was deleted, or is the wrong kind: a card id on /qr-codes/:id answers 404, and so does a QR code id on /cards/:id.
- How to fix it
- Use the id from the object you created or listed, on the matching resource.
- Retry?
- No
{
"error": {
"type": "not_found",
"code": "resource_not_found",
"message": "Card not found"
}
}404 unknown_endpoint
- Meaning
- The path or method is not part of the API.
- Likely cause
- A typo in the path, a missing /api/public/v1 prefix, or the wrong HTTP method.
- How to fix it
- Compare the method and path with the API reference.
- Retry?
- No
{
"error": {
"type": "not_found",
"code": "unknown_endpoint",
"message": "No such endpoint: PUT /api/public/v1/cards/cmf3k2a9x0001"
}
}422 validation_failed
- Meaning
- The request body or the query string did not pass validation.
- Likely cause
- A wrong type, a value outside its allowed set, a string over its limit, or an unknown key on an endpoint that rejects them.
- How to fix it
- Read details: for a body, each entry names the field by dotted path. Every problem is reported at once.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": [
{
"field": "fields.links.0.type",
"message": "Invalid enum value",
"code": "invalid_enum_value"
},
{
"field": "shortCode",
"message": "Slugs can only contain letters, numbers, and hyphens",
"code": "invalid_string"
}
]
}
}422 validation_failed(template rule)
- Meaning
- The card’s template does not allow what was sent.
- Likely cause
- The request wrote to a field the template locks or hides, or left a required field empty.
- How to fix it
- Read the template’s policy with GET /templates/:id and send only the fields it allows. details names each field and the reason.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "Some fields could not be saved.",
"details": [
{
"key": "company",
"reason": "locked"
},
{
"key": "firstName",
"reason": "required"
}
]
}
}400 invalid_request(slug taken)
- Meaning
- The custom shortCode is already taken.
- Likely cause
- Slugs are unique across every QRBold account, and a slug held by something in Trash is still held.
- How to fix it
- Choose another slug, or leave shortCode out and let one be generated.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_request",
"message": "This short code / slug is already in use. Please choose another."
}
}400 reserved_short_link
- Meaning
- The custom shortCode is a word the platform keeps for itself.
- Likely cause
- Words such as login, dashboard, api, pricing and help cannot be used as slugs.
- How to fix it
- Choose another slug.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "reserved_short_link",
"message": "\"login\" is reserved by the platform. Please choose another."
}
}409 already_exists
- Meaning
- A record with a value that must be unique already exists.
- Likely cause
- Two requests raced for the same slug and this one lost.
- How to fix it
- Read the resource to see whether your other request created it, then continue or choose another value.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "conflict",
"code": "already_exists",
"message": "A record with this value already exists."
}
}409 idempotency_key_reused
- Meaning
- This Idempotency-Key was already used with a different request.
- Likely cause
- The same key was sent with another body, path or query. That is a new request, not a retry.
- How to fix it
- Generate a new key for each logical operation and reuse it only when repeating that exact request.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "conflict",
"code": "idempotency_key_reused",
"message": "This Idempotency-Key was already used with a different request body. Use a new key for a new request."
}
}409 idempotency_key_in_flight
- Meaning
- The first request with this Idempotency-Key is still running.
- Likely cause
- A retry arrived before the original finished.
- How to fix it
- Wait a few seconds and send the same request again.
- Retry?
- Yes, with the same Idempotency-Key
{
"error": {
"type": "conflict",
"code": "idempotency_key_in_flight",
"message": "An identical request is still being processed. Retry in a few seconds."
}
}400 invalid_idempotency_key
- Meaning
- The Idempotency-Key header is too long.
- Likely cause
- The key is over 255 characters.
- How to fix it
- Use a UUID.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_idempotency_key",
"message": "Idempotency-Key must be 255 characters or fewer."
}
}409 api_not_configured
- Meaning
- The campaign has never had an API configuration activated.
- Likely cause
- The campaign exists but its API was not set up.
- How to fix it
- Open the campaign’s API tab in the dashboard, configure it and activate it.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "conflict",
"code": "api_not_configured",
"message": "This campaign has no active API configuration. Configure it in the dashboard first."
}
}409 duplicate_external_id
- Meaning
- A card with that identifier already exists in the campaign.
- Likely cause
- The same person was sent twice. This is the duplicate protection working.
- How to fix it
- Treat it as “already created”. To change the card, find it with GET /cards and update it.
- Retry?
- No
{
"error": {
"type": "conflict",
"code": "duplicate_external_id",
"message": "A card with external id \"EMP-1001\" already exists in this campaign."
}
}409 duplicate_in_trash
- Meaning
- A card with that identifier is sitting in the account’s Trash.
- Likely cause
- The earlier card for this person was deleted but not purged.
- How to fix it
- Restore it from Trash in the dashboard, or delete it permanently there, then send the request again.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "conflict",
"code": "duplicate_in_trash",
"message": "A card with external id \"EMP-1001\" is in this account's Trash (\"Ada Lovelace\", deleted 2026-10-02). Restore it, or delete it permanently, before creating another card for the same person."
}
}409 short_link_already_exists
- Meaning
- The custom short link the campaign resolved from the payload is taken.
- Likely cause
- Campaign endpoints only. Another card or code already uses that slug.
- How to fix it
- Send a different value in the field the campaign reads the short link from.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "conflict",
"code": "short_link_already_exists",
"message": "The short link \"ada-lovelace\" is already taken."
}
}422 missing_external_id
- Meaning
- The payload did not carry your identifier for the person.
- Likely cause
- The field the campaign reads the identifier from is absent.
- How to fix it
- Include it. In native mode it is the top-level externalId key.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "missing_external_id",
"message": "No value at \"employee.id\". Every request must carry your own identifier for the person."
}
}422 invalid_external_id
- Meaning
- The identifier is not usable.
- Likely cause
- It is empty, a list or an object, or longer than 128 characters.
- How to fix it
- Send a single string or number of 128 characters or fewer.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_external_id",
"message": "\"employee.id\" must be 128 characters or fewer."
}
}422 invalid_short_link
- Meaning
- The custom short link in a campaign payload is not valid.
- Likely cause
- It has characters other than letters, numbers and hyphens, is too long, is empty, or is a reserved word.
- How to fix it
- Send a valid slug in that field.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_short_link",
"message": "\"ada lovelace\" is not a valid short link. Use letters, numbers and hyphens, up to 100 characters."
}
}422 invalid_payload
- Meaning
- A native-mode campaign payload does not match the template’s schema.
- Likely cause
- It contains keys or block properties the schema does not declare, and the campaign has strict keys switched on.
- How to fix it
- Fetch the schema with GET /campaigns/:campaignId/schema and send only what it lists. details names each offending key.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_payload",
"message": "This payload does not match the template schema.",
"details": [
{
"field": "frstName",
"message": "Unknown key."
}
]
}
}400 invalid_json
- Meaning
- The request body is not valid JSON.
- Likely cause
- A trailing comma, an unquoted key, or a body that was cut off. Returned in this form by the campaign endpoints.
- How to fix it
- Serialize the body with a JSON library and send Content-Type: application/json.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_json",
"message": "The request body is not valid JSON."
}
}400 invalid_json_structure
- Meaning
- The body is valid JSON but not an object.
- Likely cause
- An array or a bare value was sent to a campaign endpoint.
- How to fix it
- Send one JSON object per request. One request creates one card.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "invalid_json_structure",
"message": "The request body must be a JSON object. One request creates one card."
}
}400 payload_too_large
- Meaning
- The request body is over the size limit.
- Likely cause
- A campaign request body is over 1 MB.
- How to fix it
- Send one person per request, and send image links rather than embedded image data.
- Retry?
- Only after changing the request or the account
{
"error": {
"type": "invalid_request",
"code": "payload_too_large",
"message": "The request body is too large. One card per request, under 1 MB."
}
}429 Rate limit exceeded
- Meaning
- The key has used its requests for this minute.
- Likely cause
- More requests than the plan’s per-key limit in a 60-second window.
- How to fix it
- Wait the number of seconds in the RateLimit-Reset header, then continue. Detect this by the 429 status: the body is not the standard error envelope.
- Retry?
- Yes, after the rate-limit window resets
{
"success": false,
"message": "API rate limit exceeded. Retry after the window resets — see the RateLimit-Reset header."
}500 internal_error
- Meaning
- Something failed on QRBold’s side.
- Likely cause
- A temporary fault. The message never includes internal detail.
- How to fix it
- Retry with the same Idempotency-Key, backing off between attempts. If it persists, email hello@qrbold.com with the time and the endpoint.
- Retry?
- Yes, with the same Idempotency-Key
{
"error": {
"type": "api_error",
"code": "internal_error",
"message": "Something went wrong on our end."
}
}Responses that break the pattern
Three responses do not follow the shape above. They are documented here as the API behaves today, so your code can handle them.
| Situation | What you get | Handle it by |
|---|---|---|
| Rate limit exceeded | 429 with { "success": false, "message": "…" }. There is no error object. | Checking the status code, not the body. |
Malformed JSON sent to POST /cards or POST /qr-codes | 500 with { "success": false, "message": "Internal server error" }, although the fault is in the request. Campaign endpoints answer 400 invalid_json correctly. | Always building bodies with a JSON library. If a 500 repeats exactly, suspect the body before retrying further. |
A taken shortCode | 400 with the generic code invalid_request, rather than a 409. | Treating 400 invalid_request on a request that carried a shortCode as a slug conflict. |
Troubleshooting
| Symptom | Likely causes | What to do |
|---|---|---|
| Authentication fails (401) | The header is missing or misspelled; the key was copied without its prefix or with a space; the environment variable is empty in this terminal; the key was revoked or has expired. | Print the variable. Check the header is exactly Authorization: Bearer qrb_live_…. Create a new key in the dashboard. |
| The key is fine but everything answers 403 | upgrade_required_api: the plan does not include REST. key_scoped_to_campaign: the key only works on its campaign. | Upgrade to Pro or the Business card plan, or use an account-wide key. |
| One endpoint answers 403 forbidden | The key lacks that endpoint’s scope. A write scope does not include the read scope. | Create a new key with the scope the message names. |
| Card creation fails | 422: a value is invalid, or the template locks, hides or requires a field. 403 limit_exhausted_cards: no card seats left. 400: the slug is taken or reserved. 404: the templateId is not in this account. | Read error.details. Try once without templateId and shortCode to isolate the cause. |
| The card was created but a field is missing | The field name was misspelled. Card creation drops unknown keys without an error. | Compare fields in the response with the field list. |
| An update wiped other fields | PATCH /cards/:id replaces the card’s data instead of merging. | Read the card, change the fields object, send all of it back. |
| QR creation fails | 422: destinationUrl is not a full http(s) address, name is missing, or the body has a key the endpoint does not know. 403 limit_exhausted_products: the plan’s QR limit is reached. | Send a complete URL including https://, and only the documented fields. |
| The QR image download gives an error or a broken file | The response was parsed as JSON or text instead of bytes; the key has neither qr:read nor cards:read; size is outside 128 to 4096; the address was used in an <img> tag without a key. | Save the raw bytes to a file. Download on your server and host the file yourself. |
| The public URL is not found | The card or code was deleted, archived, or unpublished after being published; or the link was built by hand with the wrong path or domain. | Use the url field as returned. Check status with a GET. |
| The QR code scans but shows a “not active” or “unavailable” page | status is paused, or paused is true because the plan no longer covers it, or it is in Trash. | Set a QR code back to published; restore from Trash in the dashboard; check the plan. |
| The QR code does not scan at all | It is printed too small, has too little contrast, lost its blank margin, or was stretched. | Print at least 2 cm wide from an SVG or a large PNG, dark on light, and test a proof. |
| A dynamic code goes to the wrong place | The destinationUrl is not what you think, or a cURL test did not follow the browser-side forward. | GET /qr-codes/:id and read destinationUrl. Test in a real browser. |
| The wrong card appears | A QR image was saved under the wrong person’s name, or an id was mixed up in your own records. | Scan the image and compare the address with the card’s url. |
| Created records do not show in the dashboard | You are signed in to a different account from the one the key belongs to, or a filter is hiding them. | GET /me shows the account’s email. Compare it with the dashboard’s. |
| The response is HTML, or has “success: false” instead of “error” | The request went to https://qrbold.com or to https://api.qrbold.com/api/v1 (the dashboard’s private API), or it is a 429 or a malformed-JSON 500. | Send it to https://api.qrbold.com/api/public/v1. See the exceptions below. |
| A browser request is blocked by CORS | The API is built to be called from servers. Browsers on other sites are not allowed to call it. | Call the API from your server. Never put the key in a web page. |
| Requests start failing with 429 | More requests in a minute than the key allows, often several workers sharing one key. | Wait RateLimit-Reset seconds, then pace the job. |
| A campaign endpoint refuses everything | campaign_api_disabled: the API switch is off or the campaign is not active. api_not_configured: it was never set up. | Configure and switch on the API on the campaign’s API tab in the dashboard. |
Still stuck? Email hello@qrbold.com with the time of the request, the method and path, the status and error.code, and the Request-Id header if there was one. Do not send your API key.
Which errors to retry
| Status | Code | How |
|---|---|---|
409 | idempotency_key_in_flight | Yes, with the same Idempotency-Key |
429 | (no code) | Yes, after the rate-limit window resets |
500 | internal_error | Yes, with the same Idempotency-Key |
| — | Timeout or network error | Yes, if the request was a GET or carried an Idempotency-Key. |
Everything else is a 4xx that will fail identically until the request, the key or the account changes. Retrying those only uses up your rate limit. Retry rules in full are under timeouts and retries.