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
Success envelopes
Section titled “Success envelopes”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 |
Error envelope
Section titled “Error envelope”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.
Problem fields
Section titled “Problem fields”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;suggestionsandhelp_url: for an unknown path, the nearest registered actions and the/helpfor the deepest scope that exists;help_url: for an action failure, that action’s scoped/help;docs_urlandheader: 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.
Status codes
Section titled “Status codes”| 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 |
404 — unknown path vs missing resource
Section titled “404 — unknown path vs missing resource”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.
409 — state-machine conflict
Section titled “409 — state-machine conflict”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.
422 — validation
Section titled “422 — validation”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)"] }Idempotency
Section titled “Idempotency”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: trueheader (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,errorsanddetails(each detail’svalueisnull). For a success, the first response body when it is at most 4 KB and has no customer data; otherwiseidempotent_replay_refwith the actionpathand theidof what was written. Read that resource again for its current state. - The same key with a different request returns
422with codeidempotency_key_reused. - A retry while the first request is still running returns
409with codeidempotency_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_invalidotherwise).
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.
Stable error messages
Section titled “Stable error messages”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.
Error codes
Section titled “Error codes”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 |
Thanks for letting us know.
Still stuck? Ask Quinn or chat with support