Errors
Every failed request to https://api.meykt.com/v1 returns the same JSON envelope with a machine-readable code. Codes are a permanent part of the API contract: they are added over time, but never renamed or repurposed within v1. Warnings are a separate concept — a warning means the request succeeded.
#Error format
A failed request returns a single top-level error object. There is no data key on an error response.
{
"error": {
"code": "validation_failed",
"message": "The order was rejected. No part of it was accepted — fix the listed fields and resend the complete order.",
"doc_url": "https://www.meykt.com/en/developers/errors/validation_failed",
"errors": [
{
"code": "validation_failed",
"message": "Number must be greater than or equal to 1",
"field": "items.0.quantity"
},
{
"code": "validation_failed",
"message": "String must contain at least 1 character(s)",
"field": "items.1.name"
}
]
}
}| Field | Always present | Meaning |
|---|---|---|
code | yes | Stable machine identifier. Branch on this. |
message | yes | Human-readable English text. May change at any time. |
field | no | Path to the offending field, when a single field caused the failure. |
doc_url | yes | Link to the reference page for this code. |
errors | no | Present on validation failures: all problems found, not just the first. |
#code
code is the only value you should program against. It is stable forever — renaming a code would break existing integrations and is only possible in a new API version. Never match on message, and never parse the HTTP status alone: several distinct codes share the status 422.
New codes may be added at any time. Treat an unrecognised code as a generic failure of its HTTP status class rather than crashing.
#message
message targets a human reading a log or a terminal. The wording, punctuation and level of detail can change without notice. It often carries the specific detail that code cannot express — for example insufficient_scope names the missing scopes, and file_type_not_allowed names the file type that was actually detected. Log it; do not branch on it.
#field
field names the exact location in your request body. It is present when the failure can be attributed to one place, and it also appears on each entry inside errors[].
Two notations occur, depending on where the check happened:
- Schema validation uses dot notation for array indexes:
items.0.quantity,items.1.name,currency. - File reference and warning paths use bracket notation:
items[0].files[0].upload_id,items[2].sku.
Do not build a parser that assumes one notation. Treat field as an opaque label for display and logging.
#doc_url
doc_url points at the reference page for that code. Every code in the table below is addressable at /developers/errors/<code>, so an automated client can fetch an explanation for a failure it has not seen before without a lookup table.
#errors
On validation_failed, the API validates the whole request and returns every problem at once (up to 50 entries), so you do not have to fix one field per round trip. Each entry has the same code / message / field shape as the outer object.
Order creation is all-or-nothing: if errors[] is present, nothing was created. Correct all listed fields and resend the complete order — not just the corrected part.
#Error codes
| Code | HTTP | Cause | Fix |
|---|---|---|---|
invalid_api_key | 401 | Key missing, unknown, revoked or expired | Check the Authorization: Bearer … header; verify in the dashboard that the key is still active; create a new one if needed |
insufficient_scope | 403 | The key does not carry the required scope | The message names the missing scopes; create a key with the matching scopes (there is no scope hierarchy — orders:write does not include orders:read) |
invalid_json | 400 | The body is not valid JSON | Check the request body — usually a missing quote or a trailing comma |
validation_failed | 422 | One or more fields invalid — nothing was created | Walk errors[]; each entry names a field and a reason; resend the corrected complete order |
order_already_exists_with_different_content | 409 | The external_order_id already exists with different content | Do not invent a new number if this is meant to be the same order — align the content or resolve it in the dashboard. Use a new external_order_id only for a genuinely new order |
file_not_found | 422 | upload_id unknown, never uploaded, or expired | Call POST /v1/uploads, PUT the bytes to the returned upload_url, then reference the upload_id. Unreferenced uploads expire after 14 days and must be re-uploaded |
file_type_not_allowed | 422 | The file content is not an allowed type (the message names the detected type) | See the file reference for allowed types. Renaming the extension does not help — the actual content is inspected |
not_found | 404 | Unknown path, or an order id that is unknown or belongs to another integration | Check the path against the reference. Order ids come from your own create and list responses |
rate_limited | 429 | More than 120 requests per minute on one key | Wait for Retry-After, then retry. Batch reads (GET /v1/orders?updated_since=…) instead of polling single orders |
internal_error | 500 | A failure on our side | Resend the identical body — duplicate protection makes this safe. Contact support if it repeats |
service_unavailable | 503 | The API is temporarily unavailable | Wait briefly and retry |
#Webhook endpoint errors
These codes only occur when managing webhook endpoints.
| Code | HTTP | Cause | Fix |
|---|---|---|---|
webhook_url_invalid | 422 | The target URL is not a valid https URL | Only https is accepted; http:// is rejected |
webhook_target_blocked | 422 | The target resolves to a private or reserved network address | Use a publicly reachable host. If you cannot expose one, poll GET /v1/orders?updated_since=… instead |
#File errors are nested
file_not_found and file_type_not_allowed never appear as the top-level error.code. File problems reject the order as a validation failure, so the top-level code is validation_failed (HTTP 422) and the specific file code sits inside errors[]:
{
"error": {
"code": "validation_failed",
"message": "The order was rejected because of file problems. Nothing was created.",
"doc_url": "https://www.meykt.com/en/developers/errors/validation_failed",
"errors": [
{
"code": "file_not_found",
"message": "No uploaded file found for upload_id 6f1c…",
"field": "items.0.files.0.upload_id"
}
]
}
}Match on error.code first, then walk errors[] for the detail.
#Warnings
A warning is not an error. Warnings appear on a successful POST /v1/orders response (201), alongside the created order:
{
"data": {
"order": {
"id": "…",
"external_order_id": "ORDER-2026-1044",
"is_test": true,
"items": [{ "sku": "BECHER-GROSS", "product_linked": false }]
},
"warnings": [
{
"code": "item_product_unmatched",
"field": "items[0].sku",
"value": "BECHER-GROSS",
"message": "SKU \"BECHER-GROSS\" is not linked to any product yet. The order was accepted; link it in the dashboard under Products → assignment queue."
}
]
}
}The order was accepted and stored. Do not treat warnings as a reason to retry, to change the external_order_id, or to abort your import run.
| Warning code | Meaning | Action |
|---|---|---|
item_product_unmatched | The sku does not match any Meykt product yet, so product_linked is false on that item | Link it once in the dashboard under Products → assignment queue. The link applies retroactively and to all future orders with that SKU |
item_missing_sku | The item was sent without a sku and therefore can never be matched automatically | Send a sku for items that should map to a Meykt product |
Warnings are advisory metadata. A repeated request that hits duplicate protection returns 200 with "warnings": [] — an empty array is not a signal that a previous warning was resolved.
#Retrying safely
Retries are safe by design because POST /v1/orders is deduplicated on external_order_id: sending the identical body again returns 200 with the same order, never a duplicate. Reuse the same upload_id values in the retry — duplicate detection is computed over upload_ids, not over storage paths.
Safe to retry with the identical body:
500internal_error— a failure on our side; the order may or may not have been stored, and the retry resolves that either way.503service_unavailable— the API is temporarily out of service.429rate_limited— wait for the seconds given in theRetry-Afterheader first. The limit is 120 requests per minute per key;X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Resetare returned on every response.- Network timeouts and dropped connections where you never saw a response — this is the case duplicate protection exists for.
Do not retry unchanged (fix first):
400invalid_json,422validation_failed,422file_not_found,422file_type_not_allowed— the same body will fail identically. Correct it, then resend the complete order.401invalid_api_key,403insufficient_scope— a key or scope problem; retrying burns rate limit without changing anything.404not_found— the path or id is wrong.409order_already_exists_with_different_content— the number is taken by an order with different content. Meykt never overwrites an order through the API, and there is no endpoint to change an already-sent order; resolve it in the dashboard, or use a newexternal_order_idonly if this really is a different order.
A workable client policy: retry 5xx and 429 with exponential backoff (for example 1 s, 5 s, 30 s, 2 min) and a bounded number of attempts, honouring Retry-After when present; route everything else to a dead-letter queue for a human to inspect.
Treat
200and201identically when creating an order.201means the order was newly created,200means an identical order already existed. Both return the same order object underdata.order.
Machine-readable version of this page: https://www.meykt.com/developers/errors.md