Documentation Index
Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
Use this file to discover all available pages before exploring further.
API Error Reference
This reference documents HTTP API error responses, their meanings, and how to handle them in your integration.
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
| Status | Meaning | Description |
|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 204 | No Content | Request succeeded, no content returned |
4xx Client Errors
| Status | Meaning | Description |
|---|
| 400 | Bad Request | Invalid request format or parameters |
| 401 | Unauthorized | Authentication required or invalid |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | Resource state conflict |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
5xx Server Errors
| Status | Meaning | Description |
|---|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Temporary unavailability |
| 504 | Gateway Timeout | Upstream 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:
| Error | Description |
|---|
unauthorized | No authentication provided |
invalid_token | Token is malformed or invalid |
token_expired | Authentication token has expired |
invalid_credentials | Wrong 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:
| Conflict | Description |
|---|
already_in_queue | User already joined this queue |
already_registered | User already registered for draw |
already_exists | Resource with this ID already exists |
concurrent_modification | Resource 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:
| Type | Description |
|---|
required | Field is required |
email | Must be valid email format |
min_length | Below minimum length |
max_length | Exceeds maximum length |
pattern | Doesn’t match expected pattern |
unique | Value 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 Type | Limit | Window |
|---|
| Authentication | 10 | 1 minute |
| Queue entry | 5 | 10 seconds |
| General API | 100 | 1 minute |
| Webhook | 1000 | 1 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");
}