API Reference

Error Handling

API error codes and how to handle them

The Keva public v1 ticket API uses conventional HTTP response codes. This guide covers the route-generated errors implemented by the current ticket and message surface.

HTTP Status Codes

CodeMeaning
200Success
201Created
204No Content (CORS preflight only)
400Bad Request - Invalid parameters
401Unauthorized - Invalid or missing API key
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
409Conflict - Direct public outbound delivery is unavailable
413Payload Too Large - Request body exceeds the endpoint's size limit
429Too Many Requests - Rate limit exceeded
500Internal Server Error
503Service Unavailable - API key authentication temporarily unavailable

Error Response Format

Route-generated client errors return a JSON object with an error field:

{
  "error": "Description of the error"
}

Validation errors may include additional details:

{
  "error": "Validation failed",
  "details": [
    { "field": "email", "message": "Invalid email format" },
    { "field": "subject", "message": "Subject is required" }
  ]
}

Common Errors

Authentication Errors (401)

Missing Authorization Header

{ "error": "Missing or invalid Authorization header" }

Solution: Include the Authorization: Bearer <api_key> header.

Invalid API Key Format

{ "error": "Invalid API key format" }

Solution: API keys must use keva_live_ followed by exactly 64 lowercase hexadecimal characters.

Invalid API Key

{ "error": "Invalid API key" }

Solution: Check that the API key is correct and hasn't been revoked.

Revoked API Key

{ "error": "API key has been revoked" }

Solution: Generate a new API key in Settings > API Keys.

Expired API Key

{ "error": "API key has expired" }

Solution: Generate a new API key. The current product does not expose expiry editing.

Authorization Errors (403)

Insufficient Scope

{ "error": "Insufficient scope" }

Solution: Your API key doesn't have the required scope. Create a new key with appropriate permissions.

Validation Errors (400)

Malformed JSON, invalid field values, missing required fields, and unsupported request fields return 400. Unsupported fields are not silently ignored by public v1 ticket and message writes.

Invalid Status

{ "error": "Invalid status. Must be one of: open, pending, escalated, solved, closed" }

Solution: Use one of the valid status values.

Missing Required Field

{
  "error": "Validation failed",
  "details": [
    { "field": "customerEmail", "message": "customerEmail is required" }
  ]
}

Solution: Include all required fields in your request.

Not Found Errors (404)

Malformed ticket path IDs fail with 400 and Ticket ID must be a UUID before any database lookup. A well-formed UUID that is absent from the key's workspace returns 404.

Resource Not Found

{ "error": "Ticket not found" }

Solution: Verify the resource ID and that it belongs to your tenant.

Rate Limiting (429)

{ "error": "Rate limit exceeded" }

Solution: Wait before retrying. The public contract does not currently include a Retry-After header, so use a client-side bounded delay.

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
 
    if (response.status === 429) {
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
      continue;
    }
 
    return response;
  }
  throw new Error('Max retries exceeded');
}

API-key Rate Service Unavailable (503)

{ "error": "API key authentication temporarily unavailable" }

Solution: Retry later with bounded backoff. This response means the selected strict API-key source or full-key Redis boundary was unavailable before database authentication. It does not prove a general service outage, and the public contract does not currently include a Retry-After header.

Server Errors (500)

{ "error": "Internal server error" }

Solution: Retry idempotent reads with bounded backoff. Do not automatically retry a create, update, or message write after an ambiguous server/transport failure; first read back the target state or use an application-level idempotency policy.

Error Handling Best Practices

1. Check Response Status

const response = await fetch('https://app.keva.support/api/v1/tickets', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});
 
if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error);
}
 
const data = await response.json();

2. Handle Specific Errors

async function createTicket(data) {
  const response = await fetch('https://app.keva.support/api/v1/tickets', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });
 
  if (response.status === 401) {
    // Handle authentication error
    throw new AuthError('Invalid API key');
  }
 
  if (response.status === 400) {
    // Handle validation error
    const { details } = await response.json();
    throw new ValidationError(details);
  }
 
  if (response.status === 429) {
    // Handle rate limiting
    throw new RateLimitError('Rate limit exceeded');
  }
 
  if (!response.ok) {
    const { error } = await response.json();
    throw new Error(error);
  }
 
  return response.json();
}

3. Implement Retry Logic

async function withRetry(fn, maxRetries = 3) {
  let lastError;
 
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
 
      // Don't retry route-generated client errors (4xx)
      if (error.status >= 400 && error.status < 500) {
        throw error;
      }
 
      // Exponential backoff
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
 
  throw lastError;
}

Getting Help

If you encounter persistent errors:

  1. Check the API documentation for correct usage
  2. Verify your API key and scopes
  3. Review the error message and details
  4. Contact support at hello@keva.support