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
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
204 | No Content (CORS preflight only) |
400 | Bad Request - Invalid parameters |
401 | Unauthorized - Invalid or missing API key |
403 | Forbidden - Insufficient permissions |
404 | Not Found - Resource doesn't exist |
409 | Conflict - Direct public outbound delivery is unavailable |
413 | Payload Too Large - Request body exceeds the endpoint's size limit |
429 | Too Many Requests - Rate limit exceeded |
500 | Internal Server Error |
503 | Service 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:
- Check the API documentation for correct usage
- Verify your API key and scopes
- Review the error message and details
- Contact support at hello@keva.support