How LinkHarbor API error responses are structured, what the most important error codes mean and how your code should react to them.
Every error response from the API has the same format. Your code should rely on the error code in the error field, not on the text in message – the code stays stable, while the text can change and is translated into the account language.
Structure of an error response
{
"error": "validation_failed",
"message": "Some of the information provided is invalid. Please check and try again.",
"details": [
{ "field": "title", "reason": "minLength", "message": "…" }
],
"request_id": "6b1f0c2e-…"
} - error
- Machine-readable code, for example not_found. For most errors it is also in the X-Error-Code header – but rely on the field in the body.
- message
- Human-readable description in the account language: with an API key that of the workspace owner, with a connected app that of the person who connected it. Without a valid token – for example 401 from the token check – the language follows Accept-Language, otherwise it is English. The Content-Language header names the language.
- details
- Only present when there is more to say. For 422 from input validation, a list with field, reason and message for each problem; for 422 from business logic and all other errors, an object, for example with reason, retry_after_sec or a list of issues. When a size is exceeded it contains field, reason and limit, and for 413 request_too_large max_bytes and max_kb. So check whether details is a list.
- request_id
- The ID of the request. It is also in the X-Request-Id header. Include it when you contact support.
Exceptions: the 429 rate_limited response has the field retry_after_sec instead of details; with 429 and 403 insufficient_scope, request_id is missing from the body. 401 from the token check and 422 from the schema check have no X-Error-Code header, and with the schema check only the top-level message is translated; the texts inside details stay in English. The X-Request-Id header is always set.
The most important codes
| HTTP | error | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_json | The body is not valid JSON | Check the JSON |
| 400 | bad_request | Wrong Content-Type or malformed request | Set Content-Type: application/json |
| 401 | missing_bearer_token | No token in the Authorization header | Set the header Authorization: Bearer … |
| 401 | invalid_token | Token expired or invalid, or key deleted | Get a new token; if that does not help, check the key |
| 401 | invalid_api_key | Wrong key ID or secret in the token exchange | Check the credentials |
| 402 | plan_limit_reached | Plan limit reached, for example for short links | Upgrade or delete entries |
| 403 | insufficient_scope | Scope missing or endpoint not enabled for API keys | Create a key with the right scope |
| 403 | forbidden | No permission for this action | Do it in the dashboard |
| 403 | feature_not_available | Feature not included in the plan | Check the plan |
| 403 | block_type_requires_plan | Block type only available in a higher plan | Choose a different block type or upgrade |
| 403 | template_locked | Premium template not available in the current plan | Choose a template from GET /v1/templates/available |
| 403 | workspace_suspended, workspace_read_only, workspace_grace_expired | Workspace locked or read-only because of an outstanding payment | Sort out the payment in the dashboard |
| 404 | not_found | Resource does not exist or does not belong to your workspace | Check the ID |
| 409 | conflict | General conflict with the current state | Reload the state, change the input |
| 409 | workspace_template_in_use | Workspace template is still used by pages; the number is in details.count | Switch the pages to another template, then delete it |
| 409 | handle_taken, slug_taken | Handle or short link slug is already taken | Choose a different value |
| 409 | form_in_use, page_not_private, page_not_published, never_published | Conflict with the state: form still on a page, page not private or not published, never published | Change the state, then try again |
| 409 | page_limit_reached, link_limit_reached, block_limit_reached, form_limit_reached, asset_limit_reached, asset_storage_limit_reached, workspace_template_limit_reached | The plan's quantity or storage limit reached | Remove entries or upgrade |
| 409 | draft_not_publishable | The draft has problems; the list is in details.issues | Fix the problems, then publish again |
| 413 | request_too_large | Body larger than allowed for this endpoint (64 KB, 1 MB or 2 MB); the limit is in details.max_kb | Send less data per request, see Rate limits |
| 413 | payload_too_large | Uploaded file larger than 10 MB | Make the file smaller |
| 422 | validation_failed | Invalid input or unknown field; specifics in details | Correct the fields as described in details |
| 422 | handle_reserved, target_url_invalid, target_url_scheme, target_url_host | Handle reserved, or target URL invalid or not allowed | Choose a different value |
| 422 | workspace_template_disabled | The selected workspace template is disabled | Enable the template under “Domains & Templates” or choose another one |
| 422 | template_css_external, template_section_invalid | Workspace template: CSS loads from external servers, or a {{#if}}/{{#unless}}/{{#each}} section is placed incorrectly | Remove external addresses, or close the section within the same tag or text |
| 422 | custom_css_external | Custom CSS loads something from external servers | Remove external addresses from the CSS |
| 429 | rate_limited, api_key_locked | Too many requests, or key locked after failed attempts | Wait as long as Retry-After or details.retry_after_sec says |
| 500 | internal_error | Unexpected error | Try again later, note the request_id |
| 503 | service_unavailable | Service temporarily unavailable | Try again after a pause |
Reading input errors
With 422 validation_failed, details lists every problem separately: field is the path of the field (with slashes for nested objects, for example theme_config/button_style), reason is the rule that was broken, such as required, minLength, maxLength, enum or additionalProperties. With additionalProperties, the message says which fields are unknown – often a typo in the field name.
Recommended handling in your code
- 400, 413 and 422: do not retry, fix the request instead
- 401 invalid_token: get a new token once and retry
- 403: do not retry; check the scope, the plan or who is allowed to do this
- 404: check the ID; after a deletion this is normal
- 409: reload the state and decide whether the action is still needed
- 429: wait as long as Retry-After says, then retry – see Rate limits
- 500 and 503: retry a few times with increasing pauses
You might also find this useful
Nothing found?
Tell us what you need, and our support team will help you personally.