Skip to content
API key

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:

422 response
{
  "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"
      }
    ]
  }
}
Fields of the error object
FieldWhat it isUse it for
error.typeThe broad category, such as invalid_request or permission_error.Grouping.
error.codeA stable, specific identifier.Branching in code. Match on this.
error.messageA sentence for a person. The wording can change.Logs and error screens. Do not match on it.
error.detailsPresent on some errors: a list of the individual problems.Showing which field is wrong.

Three shapes of details

Shapes of error.details
WhenEach 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

HTTP status codes used by the API
StatusNameIn this API it means
400Bad requestThe request cannot be used as sent: a taken or reserved slug, an over-long idempotency key, or a malformed campaign body.
401UnauthorizedNo valid API key was sent.
403ForbiddenThe 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.
404Not foundThe id does not exist in this account, or the path is not part of the API.
409ConflictThe request collides with something that exists: a duplicate identifier in a campaign, or an idempotency key reused or still in flight.
422UnprocessableThe body or query is well-formed but a value is not valid, or the card’s template does not allow it.
429Too many requestsThe key’s per-minute limit was passed.
500Server errorA 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.

All error codes
StatusCodeMeaning
401invalid_api_keyThe request did not carry a key the API accepts.
403forbiddenThe key is valid but does not hold the scope this endpoint needs.
403upgrade_required_apiThe account’s plan does not include the REST API.
403key_scoped_to_campaignA campaign-scoped key was used somewhere it is not allowed.
403limit_exhausted_cardsThe account has used every card its plan covers.
403limit_exhausted_productsThe account has reached its plan’s QR code limit.
403campaign_api_disabledThe campaign is not accepting API requests.
404resource_not_foundNothing with that id exists in this account.
404unknown_endpointThe path or method is not part of the API.
422validation_failedThe request body or the query string did not pass validation.
422validation_failedThe card’s template does not allow what was sent.
400invalid_requestThe custom shortCode is already taken.
400reserved_short_linkThe custom shortCode is a word the platform keeps for itself.
409already_existsA record with a value that must be unique already exists.
409idempotency_key_reusedThis Idempotency-Key was already used with a different request.
409idempotency_key_in_flightThe first request with this Idempotency-Key is still running.
400invalid_idempotency_keyThe Idempotency-Key header is too long.
409api_not_configuredThe campaign has never had an API configuration activated.
409duplicate_external_idA card with that identifier already exists in the campaign.
409duplicate_in_trashA card with that identifier is sitting in the account’s Trash.
409short_link_already_existsThe custom short link the campaign resolved from the payload is taken.
422missing_external_idThe payload did not carry your identifier for the person.
422invalid_external_idThe identifier is not usable.
422invalid_short_linkThe custom short link in a campaign payload is not valid.
422invalid_payloadA native-mode campaign payload does not match the template’s schema.
400invalid_jsonThe request body is not valid JSON.
400invalid_json_structureThe body is valid JSON but not an object.
400payload_too_largeThe request body is over the size limit.
429(no code)The key has used its requests for this minute.
500internal_errorSomething 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
401 response
{
  "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
403 response
{
  "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
403 response
{
  "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
403 response
{
  "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
403 response
{
  "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
403 response
{
  "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
403 response
{
  "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
404 response
{
  "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
404 response
{
  "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
422 response
{
  "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
422 response
{
  "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
400 response
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_request",
    "message": "This short code / slug is already in use. Please choose another."
  }
}
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
400 response
{
  "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
409 response
{
  "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
409 response
{
  "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
409 response
{
  "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
400 response
{
  "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
409 response
{
  "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
409 response
{
  "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
409 response
{
  "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."
  }
}
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
409 response
{
  "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
422 response
{
  "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
422 response
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_external_id",
    "message": "\"employee.id\" must be 128 characters or fewer."
  }
}
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
422 response
{
  "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
422 response
{
  "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
400 response
{
  "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
400 response
{
  "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
400 response
{
  "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
429 response
{
  "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
500 response
{
  "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.

Responses that are not the standard error envelope
SituationWhat you getHandle it by
Rate limit exceeded429 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-codes500 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 shortCode400 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

Troubleshooting by symptom
SymptomLikely causesWhat 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 403upgrade_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 forbiddenThe 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 fails422: 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 missingThe 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 fieldsPATCH /cards/:id replaces the card’s data instead of merging.Read the card, change the fields object, send all of it back.
QR creation fails422: 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 fileThe 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 foundThe 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” pagestatus 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 allIt 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 placeThe 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 appearsA 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 dashboardYou 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 CORSThe 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 429More 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 everythingcampaign_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

Errors worth retrying
StatusCodeHow
409idempotency_key_in_flightYes, with the same Idempotency-Key
429(no code)Yes, after the rate-limit window resets
500internal_errorYes, with the same Idempotency-Key
—Timeout or network errorYes, 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.