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

HTTPcodeMeaning
400bad_requestMalformed request — bad JSON, missing required body
401unauthorizedMissing / invalid auth token
403forbiddenAuthenticated but not authorized (scope, account access, origin)
404not_foundResource doesn't exist or RLS hides it
409conflictState conflict — version mismatch, FSM violation, currency mismatch, exhausted limit
409concurrent_modificationOptimistic lock failure — expected_version didn't match
422validation_errorBody validates against types but fails domain rules — bad enum value, out-of-range number, malformed VAT
429rate_limitedStorefront API rate limit hit (per-key per-minute cap)
500internal_errorUnhandled exception in commerce-api
502bad_gatewayUpstream service (renderer, Resend) unreachable
503service_unavailableDatabase or Redis down

Common conflict reasons

details.reason (or message contains)Meaning
market_statusMarket is paused or archived
stale_cartCart state changed since last read
insufficient_stockReservation can't be made — available too low
negativeInventory adjustment would push a state below zero
already_archivedMutation on an archived row
already_redactedMutation on a GDPR-redacted customer
phaseCheckout FSM transition not allowed (e.g. shipping before address)
currencyCurrency mismatch (cart vs variant pricing, etc.)
stackableNon-stackable discount blocked by another discount
expiredDiscount, voucher, or checkout TTL has passed
usage_limitDiscount 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.