Errors
The one envelope every failure answers in, and every code the Merchant API emits.
The envelope
A failure answers with one envelope, plus the legacy top-level keys older clients read:
{
"status": "failed",
"error_code": "insufficient_scope",
"message": "This API key does not carry the 'merchant-orders-update' scope it was called with.",
"data": null,
"error": {
"code": "insufficient_scope",
"message": "This API key does not carry the 'merchant-orders-update' scope it was called with.",
"doc_url": "https://docs.usequeek.com/docs/versioning-and-errors#insufficient_scope",
"request_id": "5d1c9b3e-8f2a-4b6d-9c07-3e1f8a2b6d45"
}
}Branch on error.code, never on message. The code is stable — a new failure mode gets a
new code, and an existing code never changes meaning — while the message is written for a
human and may be reworded. error_code at the top level mirrors error.code for clients that
predate the envelope.
error key | Always | Meaning |
|---|---|---|
code | yes | One of the codes below. |
message | yes | Human-readable. Not stable. |
doc_url | yes | Deep link to the code's row in the error reference. |
request_id | yes | The X-Request-Id of the call — quote it in support. |
field | validation only | The first offending field. |
errors | validation only | Per-field messages, { field: [message, …] }. |
Validation failures answer 422 validation_failed with the offending fields:
{
"status": "failed",
"message": "The quantity change field is required.",
"error_type": "validation_error",
"errors": { "quantity_change": ["The quantity change field is required."] },
"data": null,
"error": {
"code": "validation_failed",
"message": "The quantity change field is required.",
"field": "quantity_change",
"errors": { "quantity_change": ["The quantity change field is required."] },
"doc_url": "https://docs.usequeek.com/docs/versioning-and-errors#validation_failed",
"request_id": "5d1c9b3e-8f2a-4b6d-9c07-3e1f8a2b6d45"
}
}The top-level message is the first failing rule; error.errors has all of them.
One refusal predates the envelope and keeps its older shape: a vendor header naming a
different store than the key's answers 403 as {"error": "VENDOR_ACCESS_DENIED", "message": "This connection is bound to a different store"}. Switch on that top-level error key for
this one code only — everything else is in the envelope above.
Every code
The codes this API emits, identical to the error.code enum in the merchant OpenAPI document
(components.schemas.QueekError). Codes the shared vocabulary defines for other surfaces —
the dashboard, checkout, AI tools — are listed at the end, so nothing in the enum is
unaccounted for.
Keys and scopes
error.code | Status | Cause |
|---|---|---|
invalid_client_key | 401 | The key is unknown. Check it under Dashboard → Settings → API keys. |
api_key_mode_mismatch | 401 | A _test_ key on a live store, or a _live_ key on a dev store. Keys never cross modes. |
api_key_revoked | 403 | The key was revoked. Mint a fresh one. |
api_key_expired | 403 | The key expired. Mint a fresh one. |
private_key_required | 403 | A public pk_ key was sent. Mint a private sk_ key. |
insufficient_scope | 403 | The key does not carry the scope this operation needs — including the contact filters on the customer list without merchant-customers-contact-read. The message names the missing scope. |
route_not_available | 403 | Not part of the Merchant API — the endpoints are exactly those in the reference. A key sent to a retired api/v1/biz/… path is refused here too, with the new /api/v1/merchant/… path named in the message: This dashboard path no longer accepts API keys. Call POST /api/v1/merchant/orders/{order}/status instead. Anything else stays dashboard-only. |
plan_inactive | 403 | The store's plan has no API access. |
Note the two statuses that differ from the Storefront API: a revoked or expired key is 403
here, and so is a route outside the Merchant API — the key is valid, the call itself is what
is refused.
Cross-store
error.code | Status | Cause |
|---|---|---|
VENDOR_ACCESS_DENIED | 403 | A vendor header named a different store than the key's. Answers in the older {"error", "message"} shape. Drop the header: the key already names its store. |
Request shape
These are the status-implied defaults: the code a status carries when no surface has named the failure more precisely.
error.code | Status | Cause |
|---|---|---|
bad_request | 400 | The request could not be understood. |
unauthenticated | 401 | Authentication is required and none that fits was presented. |
forbidden | 403 | Authenticated, but not allowed to do this. |
not_found | 404 | No such resource — an id the store does not have, for instance. |
method_not_allowed | 405 | That method is not supported on this endpoint. |
conflict | 409 | The request conflicts with the current state. |
gone | 410 | The resource existed and no longer does. |
payload_too_large | 413 | The body exceeds the size limit. |
unsupported_media_type | 415 | Send Content-Type: application/json. |
validation_failed | 422 | The body or query is invalid; see error.errors. |
too_many_requests | 429 | Rate limited — honour Retry-After. |
server_error | 500 | Queek's fault. Retry with backoff and quote the request_id. |
| 503 | Temporarily unavailable. Retry with backoff. |
Idempotency
See Idempotency & rate limits for the full contract.
error.code | Status | Cause |
|---|---|---|
idempotency_key_reuse | 409 | This Idempotency-Key was already used with a different body. Use a new key. |
idempotency_key_in_progress | 409 | A request with this key is still running. Retry in a moment; nothing ran twice. |
Catalogue writes
error.code | Status | Cause |
|---|---|---|
metaobject_reference_invalid | 422 | A product write names a metaobject reference that does not resolve. |
Never emitted here
The shared envelope vocabulary names codes for surfaces this API is not. You will not meet them on a key call, and no retry or scope will produce them:
origin_required,origin_not_allowed— public-key origin checks. Apk_key is refused withprivate_key_requiredbefore any origin is looked at.client_key_required— no merchant operation names it; a call with no key at all is refused asunauthenticated.ai_unavailable_in_test_mode— no AI route is on the Merchant API.payment_amount_mismatch— checkout's verdict, never this API's. Fulfil on theorders/paidwebhook, not on anything here.custom_gateway_forbidden,custom_gateway_plan_required,custom_gateway_kyc_required,custom_gateway_store_ineligible,custom_gateway_not_verified,custom_gateway_not_configured,custom_gateway_verification_failed— the dashboard's own errors for a merchant collecting through its own provider account.vendor_mail_sender_forbidden,vendor_mail_sender_not_configured,vendor_mail_sender_plan_required,vendor_mail_sender_verification_failed— the dashboard's mail-sender setup, not on the Merchant API.metafield_definition_missing,metafield_filter_unsupported_type,metafield_namespace_reserved,metafield_value_invalid,metaobject_definition_missing,metaobject_field_invalid,metaobject_type_reserved— catalogue-validation codes from other surfaces; the reference documents exactly which fields each operation validates.
Retries that are safe
GET calls are safe to retry as they are. For writes, send an Idempotency-Key header —
every write in the reference lists it as a parameter.
Retrying the same key with the same body replays the stored response instead of acting twice;
the same key with a different body is refused, which is the point of the header. Failures
are never stored, so a retry after an error is a real retry.
When you need help
Quote the request_id from the failing response if it carries one — it is the correlation id
that ties your call to Queek's own logs. You may also send your own with the X-Request-Id
header (8–128 characters of A-Za-z0-9_.:-) and it will be used as-is.