Error codes
Every error response uses the standard envelope:
{
"error": {
"code": "conflict",
"message": "discount is not stackable with already-applied discounts",
"details": { "applied_ids": ["308..."] }
}
}
HTTP status maps to code.
Status codes
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request — bad JSON, missing required body |
| 401 | unauthorized | Missing / invalid auth token |
| 403 | forbidden | Authenticated but not authorized (scope, account access, origin) |
| 404 | not_found | Resource doesn't exist or RLS hides it |
| 409 | conflict | State conflict — version mismatch, FSM violation, currency mismatch, exhausted limit |
| 409 | concurrent_modification | Optimistic lock failure — expected_version didn't match |
| 422 | validation_error | Body validates against types but fails domain rules — bad enum value, out-of-range number, malformed VAT |
| 429 | rate_limited | Storefront API rate limit hit (per-key per-minute cap) |
| 500 | internal_error | Unhandled exception in commerce-api |
| 502 | bad_gateway | Upstream service (renderer, Resend) unreachable |
| 503 | service_unavailable | Database or Redis down |
Common conflict reasons
details.reason (or message contains) | Meaning |
|---|---|
market_status | Market is paused or archived |
stale_cart | Cart state changed since last read |
insufficient_stock | Reservation can't be made — available too low |
negative | Inventory adjustment would push a state below zero |
already_archived | Mutation on an archived row |
already_redacted | Mutation on a GDPR-redacted customer |
phase | Checkout FSM transition not allowed (e.g. shipping before address) |
currency | Currency mismatch (cart vs variant pricing, etc.) |
stackable | Non-stackable discount blocked by another discount |
expired | Discount, voucher, or checkout TTL has passed |
usage_limit | Discount or voucher usage cap reached |
Validation errors
422 responses include a details object describing what failed.
For Pydantic-detected errors, this is the standard
[{"loc": [...], "msg": "...", "type": "..."}] array. For
domain-detected errors, fields like expected, got, min_value,
max_value, allowed_values.
Idempotency
Re-submitting a request with the same Idempotency-Key returns the
original response — same status, same body. Different request
body with the same key returns 409 with code
idempotency_conflict.
Concurrent modification (optimistic version)
Most mutating endpoints require expected_version in the body.
Returns 409 concurrent_modification with:
{
"error": {
"code": "concurrent_modification",
"message": "version conflict: expected 3, current 4",
"details": {
"expected_version": 3,
"current_version": 4
}
}
}
Re-fetch the resource and retry with the new version.
Next: Changelog.