Skip to main content
When a request fails, the API returns a JSON response with success: false and an error object containing a machine-readable code, a human-readable message, and the HTTP status code.

Error response format

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "status": 404
  }
}
success
boolean
Always false for error responses.
error
object
code
string
A machine-readable error code. Use this for programmatic error handling.
message
string
A human-readable description of the error. Suitable for logging but not guaranteed to be stable across versions.
status
number
The HTTP status code associated with the error.

Error codes

CodeHTTP statusDescription
UNAUTHORIZED401The Authorization header is missing or does not contain a Bearer token.
INVALID_TOKEN401The token is malformed, expired, or has been revoked. Refresh the session and retry.
FORBIDDEN403The authenticated user does not have permission to access this resource or perform this action.
NOT_FOUND404The requested resource does not exist or the user does not have access to it.
VALIDATION_ERROR400The request body is missing required fields, contains invalid data, or has malformed JSON.
AGENT_NOT_FOUND404The specified genie (agent) does not exist or the user does not have access to it.
INVALID_ACTION400The action value is not supported for the specified resource.
RATE_LIMIT_EXCEEDED429Too many requests. Wait before retrying.
INTERNAL_ERROR500An unexpected server error occurred. If this persists, contact support.

Handling errors in code

With ApiService

ApiService.invoke() throws on error. Wrap calls in try/catch:
import { ApiService } from "@/services/api/ApiService";

try {
  const genie = await ApiService.invoke({
    resource: "genies",
    action: "get",
    id: "abc-123",
  });
} catch (error) {
  console.error("API request failed:", error.message);
}

With fetch

When using fetch directly, check the success field in the response body:
const response = await fetch(
  "https://<project-ref>.supabase.co/functions/v1/api",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      resource: "genies",
      action: "get",
      id: "abc-123",
    }),
  }
);

const result = await response.json();

if (!result.success) {
  const { code, message, status } = result.error;

  switch (code) {
    case "UNAUTHORIZED":
    case "INVALID_TOKEN":
      // Refresh the auth session and retry
      break;
    case "NOT_FOUND":
    case "AGENT_NOT_FOUND":
      // Resource does not exist or is not accessible
      break;
    case "VALIDATION_ERROR":
      // Fix the request body and retry
      break;
    case "RATE_LIMIT_EXCEEDED":
      // Wait and retry with exponential backoff
      break;
    default:
      console.error(`API error [${code}]: ${message}`);
  }
}

With TanStack Query

When using TanStack Query (React Query), errors propagate through the query’s error state:
import { useQuery } from "@tanstack/react-query";
import { ApiService } from "@/services/api/ApiService";

const { data, error, isError } = useQuery({
  queryKey: ["genies"],
  queryFn: () =>
    ApiService.invoke({
      resource: "genies",
      action: "all",
    }),
});

if (isError) {
  console.error("Failed to load genies:", error.message);
}

Retry strategy

Use exponential backoff for 429 and 500 errors. These are the only error codes worth retrying automatically. Authentication errors (401) require a token refresh, and validation errors (400, 403, 404) indicate a problem with the request itself.A recommended approach:
  1. Start with a 1-second delay after the first failure.
  2. Double the delay after each subsequent failure (1s, 2s, 4s, 8s…).
  3. Add random jitter (0-500ms) to avoid thundering herd problems.
  4. Give up after 3-4 retries and surface the error to the user.
const delay = (ms: number) =>
  new Promise((resolve) => setTimeout(resolve, ms));

async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: any) {
      const code = error?.code ?? error?.error?.code;
      const isRetryable =
        code === "RATE_LIMIT_EXCEEDED" || code === "INTERNAL_ERROR";

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      const backoff = 1000 * Math.pow(2, attempt);
      const jitter = Math.random() * 500;
      await delay(backoff + jitter);
    }
  }
  throw new Error("Max retries exceeded");
}

// Usage
const genies = await fetchWithRetry(() =>
  ApiService.invoke({ resource: "genies", action: "all" })
);

Common error scenarios

The most common cause is an expired access token. Supabase tokens typically expire after 1 hour. Use supabase.auth.refreshSession() to get a new token.
const { data, error } = await supabase.auth.refreshSession();
const newToken = data.session?.access_token;
Each resource supports a specific set of actions. Sending an unsupported action (e.g., action: "archive" to a resource that does not support it) returns INVALID_ACTION. Check the resource’s documentation in the API reference for supported actions.
The user’s role does not allow the requested operation. For example, a consumer user cannot create genies. See Introduction for the role permission matrix.
The record either does not exist or belongs to another user. Non-admin users can only access their own resources. Verify the id value and confirm the user has access.
Back off and retry with exponential delay. A simple implementation:
const delay = (ms: number) =>
  new Promise((resolve) => setTimeout(resolve, ms));

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: any) {
      if (
        error?.code === "RATE_LIMIT_EXCEEDED" &&
        attempt < maxRetries - 1
      ) {
        await delay(1000 * Math.pow(2, attempt));
        continue;
      }
      throw error;
    }
  }
  throw new Error("Max retries exceeded");
}