Skip to main content

API Error Reference

This reference documents HTTP API error responses, their meanings, and how to handle them in your integration.

Response Format

All API errors follow a consistent JSON format:
{
  "error": "error_type",
  "message": "Human-readable description",
  "code": "ERROR_CODE",
  "details": {}
}
For validation errors:
{
  "error": "validation_error",
  "issues": [
    {
      "kind": "validation",
      "type": "required",
      "expected": "string",
      "received": "undefined",
      "message": "Required field",
      "path": [{ "type": "object", "key": "email" }]
    }
  ]
}

HTTP Status Codes

2xx Success

StatusMeaningDescription
200OKRequest succeeded
201CreatedResource created successfully
204No ContentRequest succeeded, no content returned

4xx Client Errors

StatusMeaningDescription
400Bad RequestInvalid request format or parameters
401UnauthorizedAuthentication required or invalid
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn’t exist
409ConflictResource state conflict
422Unprocessable EntityValidation failed
429Too Many RequestsRate limit exceeded

5xx Server Errors

StatusMeaningDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service error
503Service UnavailableTemporary unavailability
504Gateway TimeoutUpstream service timeout

400 Bad Request

Returned when the request is malformed or contains invalid data. Example Response:
{
  "error": "bad_request",
  "message": "Invalid JSON in request body"
}
Common Causes:
  • Malformed JSON
  • Missing required fields
  • Invalid field types
  • Invalid parameter values
Resolution:
try {
  await fetch("/api/experiences", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
} catch (error) {
  if (error.status === 400) {
    console.error("Invalid request:", error.message);
    // Fix request data and retry
  }
}

401 Unauthorized

Returned when authentication is missing or invalid. Example Response:
{
  "error": "unauthorized",
  "message": "Authentication required"
}
Response Variants:
ErrorDescription
unauthorizedNo authentication provided
invalid_tokenToken is malformed or invalid
token_expiredAuthentication token has expired
invalid_credentialsWrong username/password
Required Headers: For Consumer API:
X-Publishable-Key: pk_live_...
Authorization: Bearer <consumer_token>  (if authenticated)
For Admin API:
Authorization: Bearer <clerk_jwt>
Resolution:
// Handle 401 errors
if (response.status === 401) {
  const error = await response.json();

  if (error.error === "token_expired") {
    // Refresh the token
    await client.auth.refresh();
    // Retry the request
    return retry();
  }

  // Re-authenticate
  await client.auth.login();
}

403 Forbidden

Returned when the user is authenticated but lacks permission. Example Response:
{
  "error": "forbidden",
  "message": "You don't have permission to access this resource"
}
Common Causes:
  • Accessing another organization’s resources
  • Insufficient role/permissions
  • Resource access restricted
  • Device mismatch (fingerprint)
Resolution:
if (response.status === 403) {
  // Check if it's a device mismatch
  const error = await response.json();

  if (error.code === "FP002") {
    showMessage("Please use your original device to complete this action.");
  } else {
    showMessage("You don't have access to this resource.");
  }
}

404 Not Found

Returned when the requested resource doesn’t exist. Example Response:
{
  "error": "not_found",
  "message": "Experience not found"
}
Common Causes:
  • Invalid resource ID
  • Resource was deleted
  • Resource not yet created
  • Typo in resource path
Resolution:
if (response.status === 404) {
  // Redirect to a fallback page
  navigate("/experiences");
  showMessage("This experience is no longer available.");
}

409 Conflict

Returned when the request conflicts with current resource state. Example Response:
{
  "error": "conflict",
  "message": "Already in queue"
}
Common Scenarios:
ConflictDescription
already_in_queueUser already joined this queue
already_registeredUser already registered for draw
already_existsResource with this ID already exists
concurrent_modificationResource was modified by another request
Resolution:
if (response.status === 409) {
  const error = await response.json();

  if (error.error === "already_in_queue") {
    // Get existing queue status instead
    const status = await client.queues.getStatus(queueId);
    updateUI(status);
  }
}

422 Unprocessable Entity

Returned when request data fails validation. Example Response:
{
  "error": "validation_error",
  "issues": [
    {
      "kind": "validation",
      "type": "email",
      "expected": "valid email",
      "received": "not-an-email",
      "message": "Invalid email format",
      "path": [{ "type": "object", "key": "email" }]
    },
    {
      "kind": "validation",
      "type": "min_length",
      "expected": "8 characters",
      "received": "5 characters",
      "message": "Password must be at least 8 characters",
      "path": [{ "type": "object", "key": "password" }]
    }
  ]
}
Handling Validation Errors:
if (response.status === 422) {
  const error = await response.json();

  if (error.error === "validation_error") {
    error.issues.forEach((issue) => {
      // Get the field path
      const field = issue.path[issue.path.length - 1]?.key || "root";

      // Display error on the form field
      setFieldError(field, issue.message);
    });
  }
}
Common Validation Types:
TypeDescription
requiredField is required
emailMust be valid email format
min_lengthBelow minimum length
max_lengthExceeds maximum length
patternDoesn’t match expected pattern
uniqueValue already exists (duplicate)

429 Too Many Requests

Returned when rate limit is exceeded. Example Response:
{
  "error": "rate_limited",
  "message": "Too many requests"
}
Response Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 30
Rate Limit by Endpoint:
Endpoint TypeLimitWindow
Authentication101 minute
Queue entry510 seconds
General API1001 minute
Webhook10001 minute
Resolution:
if (response.status === 429) {
  const retryAfter = parseInt(response.headers.get("Retry-After") || "30");

  showMessage(`Please wait ${retryAfter} seconds before trying again.`);

  // Automatically retry after the delay
  await sleep(retryAfter * 1000);
  return retry();
}

500 Internal Server Error

Returned when an unexpected error occurs on the server. Example Response:
{
  "error": "internal_error",
  "message": "An unexpected error occurred",
  "requestId": "req_abc123xyz"
}
Resolution:
if (response.status === 500) {
  const error = await response.json();

  // Log for debugging
  console.error("Server error:", error.requestId);

  // Show user-friendly message
  showMessage("Something went wrong. Please try again.");

  // Retry with exponential backoff
  if (attempt < maxRetries) {
    await sleep(Math.pow(2, attempt) * 1000);
    return retry();
  }
}
When contacting support, provide the requestId from the error response.

503 Service Unavailable

Returned when the service is temporarily unavailable. Example Response:
{
  "error": "service_unavailable",
  "message": "Service temporarily unavailable"
}
Common Causes:
  • Planned maintenance
  • Deployment in progress
  • Capacity limits
  • Upstream service issues
Resolution:
if (response.status === 503) {
  // Show maintenance message
  showMessage("Service is temporarily unavailable. Please try again shortly.");

  // Retry with backoff
  const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
  await sleep(retryAfter * 1000);
  return retry();
}

Database Error Codes

When database constraints are violated, specific error codes are returned.

Unique Constraint Violations

Example Response:
{
  "error": "validation_error",
  "issues": [
    {
      "kind": "validation",
      "type": "unique",
      "expected": "unique_value",
      "received": "duplicate_value",
      "message": "validation.unique",
      "path": [{ "type": "object", "key": "email" }]
    }
  ]
}
Common Fields:
  • email - Email already registered
  • name - Name already in use
  • sku - Product SKU already exists

Error Handling Best Practices

Global Error Handler

async function apiRequest<T>(url: string, options: RequestInit): Promise<T> {
  const response = await fetch(url, options);

  if (!response.ok) {
    const error = await response.json();

    switch (response.status) {
      case 401:
        // Handle authentication
        await handleUnauthorized(error);
        throw new AuthError(error);

      case 422:
        // Handle validation
        throw new ValidationError(error.issues);

      case 429:
        // Handle rate limiting
        const retryAfter = response.headers.get("Retry-After");
        throw new RateLimitError(parseInt(retryAfter || "30"));

      default:
        throw new ApiError(error);
    }
  }

  return response.json();
}

Retry Logic

async function withRetry<T>(
  fn: () => Promise<T>,
  options: { maxRetries?: number; baseDelay?: number } = {}
): Promise<T> {
  const { maxRetries = 3, baseDelay = 1000 } = options;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      // Don't retry client errors (4xx) except rate limits
      if (error.status >= 400 && error.status < 500 && error.status !== 429) {
        throw error;
      }

      if (attempt === maxRetries - 1) {
        throw error;
      }

      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
      await sleep(Math.min(delay, 30000));
    }
  }

  throw new Error("Max retries exceeded");
}