Error Handling
Error types, response format, retry strategies, and debugging tools.
Error Response Format
All errors return a consistent JSON envelope. Every response includes a unique request_id that you can reference when contacting support.
{
"error": {
"type": "not_found_error",
"message": "Product not found",
"code": "resource_missing",
"param": "id",
"request_id": "req_abc123def456"
}
}| Field | Type | Description |
|---|---|---|
| type | string | Machine-readable error category. Use this for programmatic handling. |
| message | string | Human-readable explanation. Do not parse programmatically — content may change. |
| code | string | Specific error code (e.g. duplicate_sku, insufficient_stock, rate_limit_exceeded). |
| param | string | null | The request parameter that caused the error. Null when not applicable. |
| request_id | string | Unique ID for this request. Always include this in support tickets. |
Error Types
All possible error types returned by the API. Switch on the type field for error handling logic.
authentication_errorInvalid or missing API key.
Common Causes
- •No x-api-key header provided
- •API key is revoked or expired
- •API key format is invalid (must start with wk_live_ or wk_test_)
Example Response
{
"error": {
"type": "authentication_error",
"message": "Invalid API key. Check that your key is correct and active.",
"code": "invalid_api_key",
"param": null,
"request_id": "req_a1b2c3d4e5f6"
}
}How to Fix
Verify your API key in the dashboard under Settings > API Keys. Ensure the key is active and you are using the correct environment (live vs test).
permission_errorAPI key lacks the required scope for this endpoint.
Common Causes
- •API key does not have the scope required by the endpoint (e.g. orders:write)
- •Attempting to access an admin-only endpoint with a restricted key
Example Response
{
"error": {
"type": "permission_error",
"message": "API key missing required scope: orders:write",
"code": "insufficient_scope",
"param": null,
"request_id": "req_b2c3d4e5f6a1"
}
}How to Fix
Create a new API key with the required scopes or update the existing key in Settings > API Keys. Each endpoint documents its required scope.
not_found_errorThe requested resource does not exist or was archived.
Common Causes
- •Resource ID is incorrect or belongs to a different store
- •Resource was deleted or archived
- •Endpoint path is misspelled
Example Response
{
"error": {
"type": "not_found_error",
"message": "Product not found",
"code": "resource_missing",
"param": "id",
"request_id": "req_c3d4e5f6a1b2"
}
}How to Fix
Double-check the resource ID. Use the list endpoint to confirm the resource exists. Archived resources are not returned by default.
invalid_request_errorValidation failed or required parameters are missing.
Common Causes
- •Required field is missing from the request body
- •Field value has the wrong type (e.g. string instead of number)
- •Value fails validation (e.g. negative price, invalid email format)
- •Malformed JSON body
Example Response
{
"error": {
"type": "invalid_request_error",
"message": "price must be a positive number",
"code": "validation_failed",
"param": "price",
"request_id": "req_d4e5f6a1b2c3"
}
}How to Fix
Check the param field to identify which parameter failed. Refer to the API reference for required fields and their expected types.
conflict_errorResource already exists or there is a state conflict.
Common Causes
- •Duplicate SKU, slug, or other unique field
- •Trying to transition a resource to an invalid state (e.g. fulfilling a cancelled order)
- •Concurrent modification conflict
Example Response
{
"error": {
"type": "conflict_error",
"message": "A product with SKU 'SUNSET-001' already exists",
"code": "duplicate_sku",
"param": "sku",
"request_id": "req_e5f6a1b2c3d4"
}
}How to Fix
Use a unique value for the conflicting field. For state conflicts, fetch the current resource state before attempting the operation.
rate_limit_errorretryableToo many requests. Rate limit exceeded for your plan.
Common Causes
- •Exceeded requests-per-minute limit for your plan
- •Burst limit exceeded (too many requests in a short window)
Example Response
{
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded. Try again in 12 seconds.",
"code": "rate_limit_exceeded",
"param": null,
"request_id": "req_f6a1b2c3d4e5"
}
}How to Fix
Wait for the duration specified in the Retry-After header. Implement exponential backoff. See the Rate Limits page for plan-specific limits.
store_mismatch_errorThe resource belongs to a different store than the API key.
Common Causes
- •URL storeId does not match the store associated with your API key
- •Attempting to access a resource owned by another store
Example Response
{
"error": {
"type": "store_mismatch_error",
"message": "API key does not belong to store cd2e1122-...",
"code": "store_mismatch",
"param": "store_id",
"request_id": "req_a7b8c9d0e1f2"
}
}How to Fix
Ensure the store ID in the URL matches the store your API key is associated with. Each API key is scoped to a single store.
api_errorretryableInternal server error. Safe to retry with exponential backoff.
Common Causes
- •Unexpected server-side failure
- •Temporary infrastructure issue
- •Database timeout on large queries
Example Response
{
"error": {
"type": "api_error",
"message": "An internal error occurred. Please retry your request.",
"code": "internal_error",
"param": null,
"request_id": "req_b8c9d0e1f2a3"
}
}How to Fix
Retry the request with exponential backoff. If the error persists, include the request_id when contacting support.
Retry Strategy
Only retry on 429 and 5xx errors. All other errors (4xx) indicate a problem with the request itself and retrying will not help.
Use exponential backoff with jitter to avoid thundering herd problems. For 429 responses, always respect the Retry-After header.
async function fetchWithRetry(url: string, opts: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, opts);
if (res.ok) return res.json();
// Don't retry client errors (except 429)
if (res.status < 500 && res.status !== 429) {
throw await res.json();
}
// For 429, use Retry-After header
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '5');
await sleep(retryAfter * 1000);
continue;
}
// Exponential backoff with jitter for 5xx
if (attempt < maxRetries) {
const delay = Math.min(1000 * 2 ** attempt, 30000);
const jitter = delay * 0.5 * Math.random();
await sleep(delay + jitter);
}
}
throw new Error('Max retries exceeded');
}| Attempt | Base Delay | With Jitter |
|---|---|---|
| 1 | 1s | 1 - 1.5s |
| 2 | 2s | 2 - 3s |
| 3 | 4s | 4 - 6s |
Request IDs
Every API response includes a request_id in both the response body (for errors) and the X-Request-Id response header (for all requests). Log these IDs on your end to correlate with server-side traces.
// The request ID is in the response header for all requests
const res = await fetch('https://whale-gateway.fly.dev/v1/stores/{store_id}/products', {
headers: { 'x-api-key': 'wk_live_...' }
});
const requestId = res.headers.get('X-Request-Id');
console.log('Request ID:', requestId); // req_abc123def456
// On error, it's also in the response body
if (!res.ok) {
const { error } = await res.json();
console.error(`[${error.request_id}] ${error.type}: ${error.message}`);
}When contacting support about a failed request, always include the request_id. This allows the team to look up the exact request in the trace logs.
Idempotency Keys
For POST and PATCH requests, include an Idempotency-Key header to safely retry without creating duplicates. The server caches the response for a given idempotency key for 24 hours.
curl -X POST https://whale-gateway.fly.dev/v1/stores/{store_id}/products \
-H "x-api-key: wk_live_..." \
-H "Idempotency-Key: create-sunset-sherbet-001" \
-H "Content-Type: application/json" \
-d '{"name": "Sunset Sherbet", "price": 45.00}'If the same idempotency key is sent again within 24 hours, the server returns the original response instead of creating a duplicate. This is especially important when retrying after network timeouts where you are unsure whether the original request succeeded.
Guidelines
- •Use a unique, deterministic key per logical operation (e.g.
order-{cartId}) - •Keys are scoped to your API key — different API keys have separate namespaces
- •GET and DELETE requests are inherently idempotent and do not require this header