Back to Developer

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"
  }
}
FieldTypeDescription
typestringMachine-readable error category. Use this for programmatic handling.
messagestringHuman-readable explanation. Do not parse programmatically — content may change.
codestringSpecific error code (e.g. duplicate_sku, insufficient_stock, rate_limit_exceeded).
paramstring | nullThe request parameter that caused the error. Null when not applicable.
request_idstringUnique 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.

401authentication_error

Invalid 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).

403permission_error

API 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.

404not_found_error

The 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.

400invalid_request_error

Validation 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.

409conflict_error

Resource 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.

429rate_limit_errorretryable

Too 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.

403store_mismatch_error

The 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.

500api_errorretryable

Internal 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');
}
AttemptBase DelayWith Jitter
11s1 - 1.5s
22s2 - 3s
34s4 - 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