Skip to content

Errors & Responses

v2 status codes, response envelope, and validation behavior.

BetaAPI v2 is in public beta: endpoints, fields and behaviour can change before general availability. See the changelog

Last updated

Successful reads and writes return the action’s data payload directly, with no wrapper. The HTTP status code is the contract:

Code When
200 OK Read completed
202 Accepted Write accepted — this is the success code for all writes. Synchronous writes return the updated resource snapshot; bulk/async actions return {job_id, status_url} — see Bulk & Async Jobs
204 No Content Action succeeded with no payload (rare)
207 Multi-Status Reserved for bulk actions reporting per-item failures

All non-2xx responses share one shape:

{
"errors": ["widget.button.text_color must be a hex color", "widget.disclaimer.border_radius must be a nonnegative integer"],
"details": [
{
"code": "invalid_params",
"message": "must be a hex color",
"path": "widget.button.text_color",
"value": "blue",
"expected": { "type": "string" }
},
{
"code": "invalid_params",
"message": "must be a nonnegative integer",
"path": "widget.disclaimer.border_radius",
"value": -4,
"expected": { "type": "integer", "minimum": 0 }
}
],
"code": "invalid_params"
}

errors is always an array of strings. Even single-error responses come back as a one-element array. A field error reads "<path> <message>".

details is always an array of objects, one per entry in errors, in the same order. Every error response has at least one. Every entry has the same five keys, always present; a key that does not apply is null:

Key What it holds
code invalid_params for a field error; otherwise the failure’s code (the response’s code), or null
message The error, without the field path. Never null
path The dotted request field the error is about, in API terms, or null
value The value you sent at path, or null (always null for a key, token, secret or password field, and for very long values)
expected The JSON Schema of the field at path (type, enum, format, minimum, …), where known, or null

Branch on details[].code and details[].path (and the top-level code), never on message text: messages may be reworded. Show errors to people.

Compatibility rule: errors and details never change shape. New information is only ever added as new keys on details entries, so a client that reads the keys it knows keeps working.

When an action can name the failure, the envelope also carries a stable code, and where they apply path (the field at fault), blocked_by (what must happen first) and retry_after (seconds):

{ "errors": ["Blue Tee has reached its preorder quantity limit (50). Turning 'Continue selling' back on would let customers order past the limit. ..."], "code": "quantity_limit_reached" }

That one is POST /preorders/offers/{id}/products/bulk_toggle_inventory_policy with policy: "CONTINUE" when every variant is at its quantity limit.

Branch on code, not on the message text; the same code is on the error’s details entry. The same fields appear on MCP execute_read / execute_write failures.

Every error, including 401, 404, 429 and 500, also carries RFC 9457 fields next to errors: (the shorter examples elsewhere on this page leave them out):

Field What it is
code Stable, machine-readable code. Every error has one.
type The code’s docs page, under Error codes
title Short summary of the code
status The HTTP status, as a number
detail What went wrong, then the next step

Where they apply, errors add:

  • value: the offending value you sent, such as the path or the id;
  • suggestions and help_url: for an unknown path, the nearest registered actions and the /help for the deepest scope that exists;
  • help_url: for an action failure, that action’s scoped /help;
  • docs_url and header: for a 401, the API key page and the header to send it in;
  • request_id: for a 500, to quote to support.
{
"errors": ["No v2 action registered for POST /preorders/offerz"],
"code": "action_not_found",
"type": "https://docs.stoqapp.com/v2/docs/api-v2/errors/action-not-found/",
"title": "No action at this method and path",
"status": 404,
"detail": "No v2 action registered for POST /preorders/offerz. No v2 action is registered for this method and path. ...",
"method": "POST",
"value": "/preorders/offerz",
"suggestions": [{ "method": "POST", "path": "/preorders/offers" }, { "method": "GET", "path": "/preorders/offers" }],
"help_url": "https://app.stoqapp.com/api/v2/external/preorders/help"
}

Responses are application/json. Send Accept: application/problem+json to get the same body as application/problem+json.

On MCP, failed tool results are flagged isError and carry the same fields, except that status keeps the status name ("not_found") and the number is in http_status. JSON-RPC errors from the MCP endpoint itself carry them in error.data.

Code Meaning Typical cause
400 Bad Request Malformed JSON Missing/invalid JSON body
401 Unauthorized Missing or invalid X-Auth-Token Wrong key or no header
404 Not Found Unknown action path, or resource not found Typo in path, deleted offer, expired job ID
409 Conflict State-machine refused the transition e.g., release on a cancelled order, set_collection on a non-collection offer
422 Unprocessable Entity Schema validation failure Missing required field, bad enum value, over a bulk cap
429 Too Many Requests Rate limit exhausted See Rate Limits
500 Internal Server Error Server bug — please report Unexpected exception

An unregistered verb+path combination:

{ "errors": ["No v2 action registered for POST /preorders/offers/nope"] }

A registered path addressing a resource that doesn’t exist in this shop:

{ "errors": ["Preorder offer not found: 5b2e..."] }

If you hit the first kind, ask the API itself: GET /help?prefix=... or GET <scope>/help returns the registered actions under any scope — see Dispatch & Discovery.

State-mutating actions check whether the transition is legal before running. If not, they return 409 with a one-line reason:

{ "errors": ["Cannot release: order is already cancelled."] }

Some 409s can be overridden with force: true on the request body (e.g., releasing an order before the balance is paid). The action’s notes in the /help manifest will say so.

Schema breaches surface as one error per field:

{ "errors": ["mode must be one of: auto, manual"] }

Caps are validation errors too:

{ "errors": ["Too many order_ids: 1200 (max 1000 per request)"] }

Send a fresh Idempotency-Key header with every write, such as a new UUID, and send the same key again only to retry that write. On MCP, pass idempotency_key to execute_write.

  • The first request with a key runs. Keys are remembered for 1 hour.
  • The same key with the same method, path and body within that hour returns the first result with an Idempotent-Replayed: true header (MCP: idempotent_replay: true), without running again. The order of keys in the body does not matter.
  • What a replay returns: for a failure, its code, errors and details (each detail’s value is null). For a success, the first response body when it is at most 4 KB and has no customer data; otherwise idempotent_replay_ref with the action path and the id of what was written. Read that resource again for its current state.
  • The same key with a different request returns 422 with code idempotency_key_reused.
  • A retry while the first request is still running returns 409 with code idempotency_key_in_progress. Wait, then resend.
  • Server errors (5xx) and rate limits (429) are not kept, so retrying them runs the write. Validation failures are kept and replayed.
  • Keys are per store, and REST and MCP keys are kept apart. A key may be 1 to 255 visible ASCII characters (idempotency_key_invalid otherwise).

Without a key:

  • Reads are safe to retry.
  • Lifecycle actions are idempotent in their effect (enabling an already-enabled offer is a no-op success).
  • Bulk submissions create a new job each time.
  • Creates are not idempotent: duplicate submissions create duplicate resources.

We treat the human-readable strings in errors as part of the API contract for cross-cutting messages — “X is required”, “Y must be one of: …”. Resource-specific 409 reasons may evolve over time; don’t pattern-match on those.

Each code has its own page with what it means, how to fix it and an example response. Codes are only ever added, never renamed or removed.

Code Status Title
auth_required 401 API key required
auth_invalid 401 API key not valid
shopify_reauth_required 401 STOQ’s Shopify access expired
insufficient_scope 403 Credential lacks the scope
action_not_found 404 No action at this method and path
not_found 404 Resource not found
conflict 409 Not allowed in the current state
invalid_params 422 Invalid parameters
validation_failed 422 Request refused
invalid_arguments 422 Invalid tool arguments
idempotency_key_invalid 422 Idempotency key not valid
unauthorized 401 Not authorized
forbidden 403 Forbidden
rate_limited 429 Rate limit exceeded
shopify_rate_limited 429 Shopify rate limit reached
upstream_unavailable 503 Upstream unavailable
shopify_error 502 Shopify call failed
batch_too_large 400 Batch too large
internal_error 500 Internal server error
unmapped_settings 422 Some settings have no writable field
unknown_integration 404 Unknown integration
invalid_state 409 The order is in the wrong state for this operation
job_in_progress 409 A job is already running on this resource
already_connected 409 The integration is already connected
already_sent 422 Already sent in this bulk operation
order_closed 422 The order is cancelled or refunded
idempotency_key_reused 422 Idempotency-Key reused with a different request
idempotency_key_in_progress 409 The first request with this Idempotency-Key is still running
plan_required 403 Needs a paid STOQ plan
quantity_limit_reached 422 Variants are at their preorder quantity limit
over_limit 422 Too many orders for one bulk operation
preflight_issues 422 The variants failed pre-flight checks
use_key_link 422 Enter API keys in STOQ, not through the API
key_rejected 422 The provider refused the API key
not_applicable 422 The integration does not apply to this store
invalid_option 422 The integration option is invalid
provider_error 422 The integration provider refused the request
delivery_failed 422 The message could not be sent
fetch_failed 422 Fulfillment orders could not be read
hold_failed 422 Shopify refused the fulfillment hold
no_fulfillment_orders 422 The order has no fulfillment orders
usage_charge_disabled 422 SMS is off because usage charges are disabled
sms_not_allowed 422 SMS is not enabled for this store
restricted_content_unverified 422 SMS needs restricted-content verification
restricted_content 422 The order has content SMS can’t carry
customer_unsubscribed 422 The customer unsubscribed from SMS
not_plus 422 Partial collection needs Shopify Plus
missing_amount 422 Amount or line items required
invalid_amount 422 Invalid amount
unknown_line_items 422 Line items are not on this order
Was this page helpful?

Still stuck? Ask Quinn or chat with support