Versioning & errors
What v1 promises, how a change is announced, and the shape every failure answers in.
One version, and it only grows
There is a single major version, v1, in the path:
https://client.usequeek.com/v1Inside v1:
- fields and endpoints may be added;
- nothing is removed, renamed, retyped, or given a new meaning;
- enum values may be added, and your code must tolerate values it does not know.
A breaking change would ship as v2 beside v1, never in place. So the integration you write
today keeps working: parse defensively, ignore unknown fields, and do not switch exhaustively
on a status string without a default branch.
The same promise covers webhook payloads. Every webhook delivery states the contract it was
built against in X-Queek-Api-Version.
How a retirement would be announced
Nothing in v1 is deprecated today. If something ever is, it is announced in the Changelog and on the responses
themselves, at least nine months before it stops working:
| Header | Meaning |
|---|---|
Deprecation | RFC 9745 — when the endpoint was marked deprecated. |
Sunset | RFC 8594 — the date it stops answering. |
Link: rel="deprecation" | Where to read what replaces it. |
Log these headers if you can. They are the earliest warning you will get.
Errors
A failure answers with one envelope, plus the legacy top-level keys older clients read:
{
"status": "failed",
"error_code": "invalid_client_key",
"message": "This key is unknown or has been turned off. Check your connection in Headless API.",
"data": null,
"error": {
"code": "invalid_client_key",
"message": "This key is unknown or has been turned off. Check your connection in Headless API.",
"doc_url": "https://docs.usequeek.com/docs/versioning-and-errors#invalid_client_key",
"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 on this page. |
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, …] }. |
Some codes carry extra keys beside these that describe the repair in data — a metafield error
names the handle and expected_type, for instance. They are listed with the code.
Validation failures answer 422 validation_failed with the offending fields:
{
"status": "failed",
"message": "The email field is required.",
"error_type": "validation_error",
"errors": { "email": ["The email field is required."] },
"data": null,
"error": {
"code": "validation_failed",
"message": "The email field is required.",
"field": "email",
"errors": { "email": ["The email 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.
The codes the Headless API emits
The OpenAPI document publishes one shared error vocabulary (components.schemas.QueekError,
also under x-queek-error-codes). It covers every Queek surface, including the Merchant API,
apps and the dashboard, so it is longer than what a storefront call can return. This page lists
the codes the Headless API emits. The Merchant API errors page lists
the ones that belong to server-side calls and apps. The status in each row is the one the code
travels with.
Keys and origins
error.code | Status | Cause |
|---|---|---|
client_key_required | 401 | No X-Client-Key header. |
invalid_client_key | 401 | Unknown key, or a key that was turned off. |
api_key_mode_mismatch | 401 | A _test_ key on a live store, or a _live_ key on a dev store. Keys never cross modes. |
origin_required | 403 | A website key was used with no Origin, or with Origin: null from a sandboxed frame. Server code needs a Server or backend connection, an app a Mobile app connection, a build in an AI tool a Website connection in Development mode. |
origin_not_allowed | 403 | This Origin is not one of the connection's allowed websites. The merchant allows it from the connection's Problems card. |
plan_inactive | 403 | The live store's plan has no API access for its own keys. A dev store always has it, and an installed app's token is never refused for it. |
agent_access_denied | 403 | An AI agent connection's key tried something beyond browsing products and building carts. |
storefront_password_required | 401 | A dev store read without the store's storefront token. |
route_not_available | 404 | Not part of this API — the endpoints are exactly those in the reference. |
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 — a slug 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. |
Pagination
error.code | Status | Cause |
|---|---|---|
starting_after_invalid | 422 | The starting_after cursor is stale, edited, from another list, or used under different filters. Restart the walk without it. |
Asking for a numbered page beyond 50 answers page_too_deep: switch to a cursor walk.
Idempotency
See Payments → Idempotency-Key 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. |
Metafields
See Metafields. Each carries handle and owner_type in error;
metafield_value_invalid adds expected_type; metafield_namespace_reserved carries
namespace.
error.code | Status | Cause |
|---|---|---|
metafield_definition_missing | 422 | A metafield[…] filter names a handle the store has not defined. |
metafield_value_invalid | 422 | The value cannot be held by the definition's type, or the field is a json / list.* type and cannot be filtered. |
metafield_namespace_reserved | 422 | A definition was requested in the reserved queek namespace. |
Payments
See Payments.
error.code | Status | Cause |
|---|---|---|
payment_amount_mismatch | 409 | The provider confirmed a different amount or currency than the transaction's. The order stays unpaid until a human reconciles it. |
custom_gateway_forbidden | 403 | Only the store owner can change where payments are collected. |
custom_gateway_plan_required | 403 | The plan does not include collecting into the merchant's own account. |
custom_gateway_kyc_required | 403 | Identity verification is not complete. |
custom_gateway_store_ineligible | 403 | A template or dev store cannot collect real payments. |
custom_gateway_not_verified | 409 | No provider account has been verified yet. |
custom_gateway_not_configured | 404 | The provider's keys have not been saved. |
custom_gateway_verification_failed | 422 | The saved keys did not pass the verification round-trip. |
AI availability
error.code | Status | Cause |
|---|---|---|
| 403 | An AI action was attempted outside a live or dev store. AI works on both. |
Retries that are safe
| Response | What to do |
|---|---|
429 too_many_requests | Wait for Retry-After seconds, then retry. |
500 server_error, 503 service_unavailable | Retry with exponential backoff and jitter. Quote request_id if it keeps failing. |
401, 403, 404, 422 | Do not retry. Fix the key, origin, id or body first. |
GET calls are safe to retry as they are. For writes, send an Idempotency-Key header —
operations that accept it document it as a parameter in the
reference. 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.