Error Handling
HTTP status codes, error format, and complete error code reference.
Error Envelope
All error responses — regardless of status code — use a consistent JSON envelope:
{
"error": {
"code": "SCREAMING_SNAKE_CASE",
"message": "Human-readable description",
"details": [
{ "field": "field_name", "message": "Description of the issue" }
]
}
}Guarantees:
- Every error response is
application/json— nevertext/plain codeis always aSCREAMING_SNAKE_CASEstringdetailsis only present forVALIDATION_ERRORresponses; omitted otherwise
HTTP Status Codes
| Status | Meaning | When |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Valid key but insufficient scope or plan limit hit |
404 | Not Found | Resource does not exist |
409 | Conflict | Duplicate resource (e.g., category name already exists) |
422 | Unprocessable Entity | Invalid input — see details[] for per-field errors |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server error |
Error Code Reference
AUTHENTICATION_ERROR — 401
Returned when the X-API-Key header is missing, malformed, invalid, expired, or inactive.
{
"error": {
"code": "AUTHENTICATION_ERROR",
"message": "Missing X-API-Key header"
}
}Other messages for this code:
"Invalid API key format"— key does not start withwk_live_orwk_test_"Invalid or inactive API key"— key not found or deactivated"API key has expired"— key's expiry date has passed
SCOPE_INSUFFICIENT — 403
Returned when the API key does not have the required scope for the endpoint.
{
"error": {
"code": "SCOPE_INSUFFICIENT",
"message": "This API key does not have the 'write:products' scope"
}
}PLAN_LIMIT_EXCEEDED — 403
Returned when a create operation would exceed the store's plan limit.
{
"error": {
"code": "PLAN_LIMIT_EXCEEDED",
"message": "Plan limit exceeded for products: 100/100"
}
}NOT_FOUND — 404
Returned when the requested resource does not exist.
{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found"
}
}Also returned for route-not-found (unknown endpoint):
{
"error": {
"code": "NOT_FOUND",
"message": "Route GET /v1/unknown not found"
}
}CONFLICT — 409
Returned when a resource already exists (e.g., duplicate name).
{
"error": {
"code": "CONFLICT",
"message": "Resource already exists"
}
}VALIDATION_ERROR — 422
Returned when input fails validation. Always includes a details array with per-field errors.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "price", "message": "Number must be greater than 0" },
{ "field": "name", "message": "Required" }
]
}
}The field property uses dot notation for nested fields (e.g., "address.city").
RATE_LIMITED — 429
Returned when the API key exceeds its per-minute request quota. Includes a Retry-After response header (seconds).
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 23 seconds."
}
}Response headers on rate-limit responses:
| Header | Value |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp (seconds) when window resets |
Retry-After | Seconds to wait before retrying |
INTERNAL_ERROR — 500
Returned for unexpected server-side failures. The message is intentionally generic — internal details are never exposed.
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred"
}
}Best Practices
- Check
error.codefirst, not just the HTTP status — multiple codes can share a status - For
VALIDATION_ERROR, iterateerror.detailsto surface field-level messages to users - For
RATE_LIMITED, respect theRetry-Afterheader with exponential backoff - Don't retry 4xx errors (except
RATE_LIMITED) — they indicate a client issue - Retry 5xx errors with backoff — they indicate temporary server issues
- Log
error.code+error.messagein your error tracking for fast debugging