Skip to main content

Documentation Index

Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt

Use this file to discover all available pages before exploring further.

The Admin API uses the same Stripe-style error format as the rest of the Spree v3 API. Every error response carries a machine-readable code and a human-readable message.

Error response format

{
  "error": {
    "code": "record_not_found",
    "message": "Order not found"
  }
}
Validation errors include a details field with per-field error messages:
{
  "error": {
    "code": "validation_error",
    "message": "Email is invalid and Phone can't be blank",
    "details": {
      "email": ["is invalid"],
      "phone": ["can't be blank"]
    }
  }
}
Some specialized errors carry structured details (for example, scope errors include the required_scope):
{
  "error": {
    "code": "access_denied",
    "message": "API key lacks scope: write_orders",
    "details": {
      "required_scope": "write_orders"
    }
  }
}

Schema

FieldTypeDescription
error.codestringMachine-readable error code (see tables below)
error.messagestringHuman-readable description of the error
error.detailsobjectOptional. Field-specific validation errors or structured context (varies by code).

HTTP status codes

StatusMeaningWhen
400Bad RequestMissing required parameters, malformed JSON
401UnauthorizedMissing or invalid API key, expired or invalid JWT token
403ForbiddenAuthenticated but lacks permission (CanCanCan ability) or scope
404Not FoundResource doesn’t exist, isn’t accessible to the calling key, or belongs to a different store
409ConflictResource was modified by a concurrent request (optimistic locking)
422Unprocessable ContentValidation failed, invalid state transition, business rule violation
429Too Many RequestsLogin/refresh rate limit exceeded

Authentication & authorization

CodeStatusDescription
authentication_required401Request reached a protected endpoint with no credentials
authentication_failed401Login email/password was wrong
invalid_token401API key or JWT token is invalid or expired
invalid_refresh_token401Refresh token is invalid or expired
access_denied403Caller lacks permission. For API-key callers, details.required_scope indicates the missing scope (see Authentication). For JWT callers, the user’s role lacks the CanCanCan ability for this action.
current_password_invalid422Current password is wrong (when changing password)

Resources

CodeStatusDescription
record_not_found404Resource doesn’t exist or isn’t accessible in the calling key’s store
resource_invalid422Resource couldn’t be saved

Validation

CodeStatusDescription
validation_error422Model validation failed. Inspect details for field-specific messages.
parameter_missing400A required parameter is missing
parameter_invalid400A parameter has an invalid value

Orders

CodeStatusDescription
cart_cannot_transition422Order state machine refused the transition (e.g., completing an order without an address or payment)
cart_already_updated409Order was modified by a concurrent request. Refetch and retry. See optimistic locking below.
cart_invalid_state422Order is in a state that doesn’t allow this operation
cart_empty422Cannot complete an order with no line items

Customers

CodeStatusDescription
customer_has_orders422Cannot delete a customer with completed orders. Anonymize instead.

Store credits

CodeStatusDescription
store_credit_in_use422Cannot edit amount on or delete a store credit that has been partially or fully used.

Tags

CodeStatusDescription
invalid_taggable_type422The taggable_type query param isn’t one of the allowed values (Spree::Product, Spree::Order, Spree::User).

Payments

CodeStatusDescription
payment_failed422Payment was declined by the gateway
payment_processing_error422Spree couldn’t process the payment due to an internal error
gateway_error422Payment gateway returned an error

Examples

Insufficient scope (API key)

curl -X POST 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{"email": "test@example.com"}'
403
{
  "error": {
    "code": "access_denied",
    "message": "API key lacks scope: write_orders",
    "details": {
      "required_scope": "write_orders"
    }
  }
}

Validation error (customer create)

curl -X POST 'https://store.example.com/api/v3/admin/customers' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{}'
422
{
  "error": {
    "code": "validation_error",
    "message": "Email can't be blank",
    "details": {
      "email": ["can't be blank"]
    }
  }
}

Customer with completed orders

curl -X DELETE 'https://store.example.com/api/v3/admin/customers/cus_xxx' \
  -H 'X-Spree-Api-Key: sk_xxx'
422
{
  "error": {
    "code": "customer_has_orders",
    "message": "Cannot delete customer with completed orders"
  }
}

Concurrent order update

curl -X PATCH 'https://store.example.com/api/v3/admin/orders/or_xxx' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{"email": "new@example.com"}'
409
{
  "error": {
    "code": "cart_already_updated",
    "message": "Order was modified by another request. Refetch and retry."
  }
}

Handling errors with the SDK

@spree/admin-sdk throws a SpreeError for every non-2xx response:
import { SpreeError } from '@spree/admin-sdk'

try {
  await client.orders.update(orderId, { email: 'new@example.com' })
} catch (error) {
  if (error instanceof SpreeError) {
    console.log(error.code)    // 'cart_already_updated'
    console.log(error.message) // 'Order was modified by another request...'
    console.log(error.status)  // 409
    console.log(error.details) // undefined or { ... }
  }
}

Common patterns

Branch on error code:
try {
  await client.customers.delete(customerId)
} catch (error) {
  if (error instanceof SpreeError && error.code === 'customer_has_orders') {
    // Anonymize instead of deleting
    return anonymizeCustomer(customerId)
  }
  throw error
}
Retry on optimistic-lock conflicts:
async function updateOrderWithRetry(id, params, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await client.orders.update(id, params)
    } catch (error) {
      if (error instanceof SpreeError && error.code === 'cart_already_updated' && i < attempts - 1) {
        continue
      }
      throw error
    }
  }
}
Show field-level validation errors:
try {
  await client.customers.create(customerData)
} catch (error) {
  if (error instanceof SpreeError && error.details) {
    for (const [field, messages] of Object.entries(error.details)) {
      setFieldError(field, (messages as string[]).join(', '))
    }
  }
}

Optimistic locking

Orders use a state_lock_version column to detect concurrent modifications. Every state-changing operation increments it; if two callers update the same order simultaneously, the second write fails with cart_already_updated (409) — refetch and retry. This protects against race conditions when multiple clients (or the same client, retried) try to mutate the same order. Combined with idempotency at the integration level (e.g., dedupe webhook deliveries by event ID), it makes admin order management safe under concurrency.