Skip to main content

Error Response Format

When an API request fails, Kordless returns a JSON error response with a consistent structure:
{
  "detail": "Human-readable error message",
  "status": 404,
  "type": "not_found"
}

Error Response Fields

detail
string
A human-readable description of the error. Use this for debugging and logging.
status
integer
The HTTP status code (400, 401, 403, 404, 429, 500, etc.)
type
string
A machine-readable error type for programmatic handling.

HTTP Status Codes

Kordless uses standard HTTP status codes to indicate success or failure:

2xx Success

Meaning: Request succeededResponse: Contains the requested data
{
  "bookings": [...]
}
Meaning: Resource successfully createdResponse: Contains the newly created resource
{
  "booking": {
    "id": "book_abc123",
    ...
  }
}
Meaning: Request succeeded with no response bodyUse case: DELETE requests, successful updates with no data to return

4xx Client Errors

Meaning: The request is malformed or contains invalid dataCommon causes:
  • Missing required fields
  • Invalid field values
  • Malformed JSON
{
  "detail": "startsAt is required",
  "status": 400,
  "type": "validation_error"
}
How to fix:
  • Check request body against API documentation
  • Validate all required fields are present
  • Ensure correct data types
Meaning: Authentication failed or credentials are missingCommon causes:
  • Missing x-kordless-key header
  • Invalid or revoked API key
  • Expired credentials
{
  "detail": "Invalid or missing API key",
  "status": 401,
  "type": "authentication_error"
}
How to fix:
  • Verify API key is included in headers
  • Check key hasn’t been revoked
  • Generate a new API key if needed
Meaning: Authentication succeeded but you lack permissionCommon causes:
  • Wrong organization ID
  • Accessing resources from another organization
  • Insufficient permissions
{
  "detail": "Insufficient permissions",
  "status": 403,
  "type": "permission_error"
}
How to fix:
  • Verify x-organization-id header is correct
  • Ensure you’re accessing your own organization’s resources
  • Check API key permissions
Meaning: The requested resource doesn’t existCommon causes:
  • Invalid resource ID
  • Resource was deleted
  • Wrong endpoint URL
{
  "detail": "Booking not found",
  "status": 404,
  "type": "not_found"
}
How to fix:
  • Verify the resource ID is correct
  • Check the endpoint URL
  • Ensure the resource exists in your organization
Meaning: The request conflicts with current stateCommon causes:
  • Time slot already booked
  • Overlapping bookings
  • Resource state conflict
{
  "detail": "Time slot is no longer available",
  "status": 409,
  "type": "conflict_error"
}
How to fix:
  • Query fresh availability
  • Choose a different time slot
  • Resolve the conflict before retrying
Meaning: Request is well-formed but semantically invalidCommon causes:
  • Business logic validation failed
  • Invalid time range
  • Rule violations
{
  "detail": "End time must be after start time",
  "status": 422,
  "type": "validation_error"
}
How to fix:
  • Read the error message carefully
  • Adjust request to meet business rules
  • Validate data before sending
Meaning: Rate limit exceededCommon causes:
  • Too many requests in short time
  • Exceeded per-minute/hour/day limits
{
  "detail": "Rate limit exceeded. Try again in 30 seconds.",
  "status": 429,
  "type": "rate_limit_error"
}
How to fix:
  • Implement exponential backoff
  • Respect rate limit headers
  • Cache responses when possible
  • Contact us for higher limits

5xx Server Errors

Meaning: An unexpected error occurred on our serversWhat to do:
  • Retry the request after a delay
  • Check our status page
  • Contact support if persistent
{
  "detail": "An internal error occurred",
  "status": 500,
  "type": "server_error"
}
Meaning: Service temporarily unavailable (maintenance or overload)What to do:
  • Retry with exponential backoff
  • Check status page for maintenance windows
  • Implement circuit breaker pattern
{
  "detail": "Service temporarily unavailable",
  "status": 503,
  "type": "service_unavailable"
}

Rate Limiting

API requests are rate limited to ensure fair usage and system stability.

Rate Limit Headers

Every response includes rate limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200
X-RateLimit-Limit
integer
Maximum requests allowed in the current window
X-RateLimit-Remaining
integer
Number of requests remaining in current window
X-RateLimit-Reset
integer
Unix timestamp when the rate limit resets

Default Limits

  • 100 requests per minute per API key
  • 1,000 requests per hour per API key
  • 10,000 requests per day per organization

Handling Rate Limits

async function makeRequestWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status === 429) {
      const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
      const waitTime = (resetTime * 1000) - Date.now();

      console.log(`Rate limited. Waiting ${waitTime}ms...`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      continue;
    }

    return response;
  }

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

Error Handling Best Practices

Don’t assume success. Always check the HTTP status code:
const response = await fetch(url, options);

if (!response.ok) {
  const error = await response.json();
  throw new Error(`API error: ${error.detail}`);
}
Retry on transient errors (500, 503, 429) with exponential backoff:
async function withRetry(fn, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isRetryable = [429, 500, 503].includes(error.status);
      const isLastAttempt = attempt === maxAttempts - 1;

      if (!isRetryable || isLastAttempt) throw error;

      // Exponential backoff: 1s, 2s, 4s...
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}
Log errors with context for debugging:
try {
  const booking = await createBooking(data);
} catch (error) {
  console.error('Booking creation failed:', {
    error: error.message,
    status: error.status,
    type: error.type,
    requestData: data,
    timestamp: new Date().toISOString()
  });

  // Rethrow or handle appropriately
  throw error;
}
Handle different error types appropriately:
try {
  const booking = await createBooking(data);
} catch (error) {
  switch (error.status) {
    case 401:
      // Refresh credentials
      await refreshApiKey();
      return createBooking(data); // Retry

    case 409:
      // Conflict - time slot taken
      const newSlots = await getAvailability();
      return showUserAlternatives(newSlots);

    case 429:
      // Rate limited - wait and retry
      await sleep(error.retryAfter);
      return createBooking(data);

    default:
      // Show user-friendly error
      showError('Unable to create booking. Please try again.');
      throw error;
  }
}
Prevent duplicate operations when retrying:
const idempotencyKey = `booking-${userId}-${Date.now()}`;

const response = await fetch('/api/calendar/v1/bookings', {
  method: 'POST',
  headers: {
    'x-kordless-key': apiKey,
    'x-organization-id': orgId,
    'Idempotency-Key': idempotencyKey,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(bookingData)
});
Monitor rate limit headers and throttle requests:
class RateLimitedClient {
  constructor() {
    this.remaining = 100;
    this.resetTime = Date.now();
  }

  async request(url, options) {
    // Wait if rate limit exhausted
    if (this.remaining === 0) {
      const waitTime = this.resetTime - Date.now();
      if (waitTime > 0) {
        await new Promise(r => setTimeout(r, waitTime));
      }
    }

    const response = await fetch(url, options);

    // Update rate limit state
    this.remaining = parseInt(
      response.headers.get('X-RateLimit-Remaining')
    );
    this.resetTime = parseInt(
      response.headers.get('X-RateLimit-Reset')
    ) * 1000;

    return response;
  }
}

Common Error Scenarios

Scenario 1: Time Slot No Longer Available

Error:
{
  "detail": "Time slot is no longer available",
  "status": 409,
  "type": "conflict_error"
}
Solution:
try {
  const booking = await createBooking(data);
} catch (error) {
  if (error.status === 409) {
    // Query fresh availability
    const slots = await getAvailability({
      serviceId: data.serviceId,
      from: data.startsAt,
      to: data.endsAt
    });

    // Show user alternative times
    return showAlternativeSlots(slots);
  }
}

Scenario 2: Invalid Organization ID

Error:
{
  "detail": "Organization not found",
  "status": 404,
  "type": "not_found"
}
Solution:
// Verify organization ID format
const ORG_ID = process.env.KORDLESS_ORG_ID;

if (!ORG_ID || !ORG_ID.startsWith('org_')) {
  throw new Error('Invalid organization ID format');
}

// Use in request
const response = await fetch(url, {
  headers: {
    'x-organization-id': ORG_ID
  }
});

Scenario 3: Rate Limit Exceeded

Error:
{
  "detail": "Rate limit exceeded. Try again in 30 seconds.",
  "status": 429,
  "type": "rate_limit_error"
}
Solution:
// Implement exponential backoff
async function withBackoff(fn, maxAttempts = 5) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.status !== 429 || attempt === maxAttempts - 1) {
        throw error;
      }

      // Wait with exponential backoff
      const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

Testing Error Handling

Test your error handling with these scenarios:
1

Test Invalid API Key

curl https://api.kordless.ai/api/calendar/v1/bookings \
  -H "x-kordless-key: invalid_key" \
  -H "x-organization-id: your_org_id"
Expected: 401 Unauthorized
2

Test Missing Required Field

curl -X POST https://api.kordless.ai/api/calendar/v1/bookings \
  -H "x-kordless-key: your_key" \
  -H "x-organization-id: your_org_id" \
  -H "Content-Type: application/json" \
  -d '{}'
Expected: 400 Bad Request
3

Test Rate Limiting

Make 101 requests rapidly to trigger rate limit.Expected: 429 Too Many Requests on 101st request

Need Help?