docs/api-reference/store-api/errors.mdx
The Store API uses a consistent, Stripe-style error format across all endpoints. Every error response includes a machine-readable code and a human-readable message.
All errors return a JSON object with a single error key:
{
"error": {
"code": "record_not_found",
"message": "Product not found"
}
}
Validation errors include an additional details field with per-field error messages:
{
"error": {
"code": "validation_error",
"message": "Name can't be blank and Email is invalid",
"details": {
"name": ["can't be blank"],
"email": ["is invalid"]
}
}
}
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code (see table below) |
error.message | string | Human-readable description of the error |
error.details | object | Field-specific validation errors. Each key is a field name, each value is an array of error strings. Only present for validation errors. |
| Status | Meaning | When |
|---|---|---|
400 | Bad Request | Missing required parameters, malformed JSON, invalid arguments |
401 | Unauthorized | Missing or invalid API key, expired JWT token |
403 | Forbidden | Authenticated but not authorized for this resource |
404 | Not Found | Resource doesn't exist or isn't accessible |
409 | Conflict | Resource was modified by another request (concurrent update) |
422 | Unprocessable Content | Validation failed, invalid state transition, payment error |
429 | Too Many Requests | Rate limit exceeded (see Rate Limiting) |
| Code | Status | Description |
|---|---|---|
authentication_required | 401 | Request requires a valid JWT token |
authentication_failed | 401 | Email or password is incorrect |
invalid_token | 401 | API key or JWT token is invalid or expired |
invalid_provider | 400 | OAuth provider not recognized |
access_denied | 403 | User doesn't have permission for this action |
| Code | Status | Description |
|---|---|---|
record_not_found | 404 | Resource doesn't exist or isn't accessible in the current store |
resource_invalid | 422 | Resource couldn't be saved |
| Code | Status | Description |
|---|---|---|
validation_error | 422 | Model validation failed. Check details for field-specific messages. |
parameter_missing | 400 | A required parameter is missing |
parameter_invalid | 400 | A parameter has an invalid value |
| Code | Status | Description |
|---|---|---|
cart_not_found | 404 | Cart doesn't exist or doesn't belong to the current user/guest |
cart_cannot_complete | 422 | Cart cannot be completed (e.g., missing payment or address) |
cart_cannot_transition | 422 | Internal state machine transition failed (e.g., completing a cart that isn't ready) |
cart_empty | 422 | Cart has no line items |
cart_invalid_state | 422 | Cart is in an invalid state for this operation |
cart_already_updated | 409 | Cart was modified by another request (optimistic locking conflict) |
| Code | Status | Description |
|---|---|---|
order_not_found | 404 | Completed order doesn't exist or doesn't belong to the current user/guest |
| Code | Status | Description |
|---|---|---|
line_item_not_found | 404 | Line item doesn't exist in this order |
variant_not_found | 404 | Variant doesn't exist or isn't available |
insufficient_stock | 422 | Not enough stock to fulfill the requested quantity |
invalid_quantity | 422 | Quantity must be a positive integer |
| Code | Status | Description |
|---|---|---|
payment_failed | 422 | Payment was declined or couldn't be processed |
payment_processing_error | 422 | Error communicating with the payment gateway |
gateway_error | 422 | Payment gateway returned an error |
| Code | Status | Description |
|---|---|---|
attachment_missing | 403 | File attachment not found for this digital product |
download_unauthorized | 403 | User is not authorized to download this file |
digital_link_expired | 403 | Download link has expired |
download_limit_exceeded | 403 | Maximum number of downloads reached |
| Code | Status | Description |
|---|---|---|
order_already_updated | 409 | Order was modified by another concurrent request. Retry with fresh data. |
idempotency_key_reused | 422 | Idempotency key was already used with different request parameters. See Idempotency. |
| Code | Status | Description |
|---|---|---|
rate_limit_exceeded | 429 | Too many requests. Retry after the Retry-After header value. |
request_too_large | 413 | Request body exceeds the size limit |
processing_error | 422 | Generic server-side processing error |
invalid_request | 400 | Request is malformed (e.g., invalid JSON body) |
curl https://api.mystore.com/api/v3/store/products/prod_nonexistent \
-H "Authorization: Bearer pk_xxx"
{
"error": {
"code": "record_not_found",
"message": "Product not found"
}
}
curl -X POST https://api.mystore.com/api/v3/store/account/addresses \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"code": "validation_error",
"message": "First name can't be blank, Address can't be blank, City can't be blank, and Country can't be blank",
"details": {
"firstname": ["can't be blank"],
"address1": ["can't be blank"],
"city": ["can't be blank"],
"country": ["can't be blank"]
}
}
}
curl -X POST https://api.mystore.com/api/v3/store/carts/cart_xxx/items \
-H "Authorization: Bearer pk_xxx" \
-H "X-Spree-Token: abc123" \
-H "Content-Type: application/json" \
-d '{"variant_id": "var_xxx", "quantity": 999}'
{
"error": {
"code": "insufficient_stock",
"message": "Quantity selected exceeds available stock"
}
}
curl -X POST https://api.mystore.com/api/v3/store/carts/cart_xxx/complete \
-H "Authorization: Bearer pk_xxx"
{
"error": {
"code": "cart_cannot_transition",
"message": "Cannot transition cart to complete — ensure address, shipping, and payment are set"
}
}
The @spree/sdk package throws a SpreeError for all non-2xx responses:
import { SpreeError } from '@spree/sdk';
try {
const product = await client.products.get('nonexistent');
} catch (error) {
if (error instanceof SpreeError) {
console.log(error.code); // 'record_not_found'
console.log(error.message); // 'Product not found'
console.log(error.status); // 404
console.log(error.details); // undefined (or field errors for validation)
}
}
Handle specific error codes:
try {
await client.carts.items.create(cartId, {
variant_id: variantId,
quantity: 1,
});
} catch (error) {
if (error instanceof SpreeError) {
switch (error.code) {
case 'insufficient_stock':
showNotification('This item is out of stock');
break;
case 'variant_not_found':
showNotification('This product is no longer available');
break;
default:
showNotification(error.message);
}
}
}
Display validation errors per field:
try {
await client.customer.addresses.create(addressData);
} catch (error) {
if (error instanceof SpreeError && error.details) {
// error.details = { city: ["can't be blank"], zipcode: ["is invalid"] }
for (const [field, messages] of Object.entries(error.details)) {
setFieldError(field, messages.join(', '));
}
}
}