Skip to main content

Errors

Every failure returns the same JSON envelope, whatever went wrong:

{
"error": {
"code": "missing_field",
"message": "Mandatory attribute missing",
"errors": [
{ "field": "channel", "code": "missing_field", "message": "Mandatory attribute missing" }
]
}
}
FieldNotes
error.codeA stable, machine-readable code. Branch on this, never on the message
error.messageA human-readable sentence, safe to log. Wording may change; treat it as prose
error.errors[]Per-field detail, present only when the failure is attributable to specific fields. Each entry has field, code and message
error.existing_idPresent only on a 409 duplicate_value. The id of the record that already holds your value — see the API reference

Internal diagnostics are never exposed. There is no stack trace, no Sentry id and no internal metadata in a public response.

Error codes

CodeHTTPMeans
missing_field400A required attribute was not supplied
invalid_value400An attribute was supplied but its value is not acceptable
datatype_mismatch400An attribute has the wrong type
invalid_field400An attribute is not part of this request's contract
invalid_json400The request body is not parseable JSON
invalid_credentials401The Authorization header is missing, malformed, or the key is unknown, revoked or expired
token_expired401The presented credential is no longer valid for time-based reasons
access_denied403Authenticated, but not allowed: a missing scope, or credentials sent in the query string
feature_not_enabled403The operation depends on a feature that is not switched on for this account or AI agent
not_found404No such record in your account. Also returned instead of 403 where confirming existence would leak information
duplicate_value409A uniqueness constraint was violated. The response carries the existing record id
immutable_state409The record is in a state that does not allow this change
inconsistent_state409The request conflicts with related data, such as deleting something that still has dependents
unsupported_type415The content type or file type is not accepted
file_too_large413An upload exceeded the permitted size
rate_limit_exceeded429The key's per-minute ceiling was hit. Read Retry-After
server_error500Something failed on our side. Safe to retry with backoff

Validation failures

Validation is applied to the query string and the body before a handler runs, and reports the offending field:

curl -X POST https://api.kaily.ai/v1/ai-agents \
-H "Authorization: Bearer kly_live_a1b2c3d4e5f6_s3cr3tvalue" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"code": "missing_field",
"message": "Mandatory attribute missing",
"errors": [
{ "field": "name", "code": "missing_field", "message": "Mandatory attribute missing" }
]
}
}

A value that exists but is not acceptable comes back as invalid_value, and the message names the acceptable set where there is one:

{
"error": {
"code": "invalid_value",
"message": "enabled must be a boolean",
"errors": [
{ "field": "enabled", "code": "invalid_value", "message": "enabled must be a boolean" }
]
}
}
Read enumerated values from the API, never hard-code them

Where a value looks like a fixed list, check the reference before assuming it is one. Several of them are configured per account, so the valid set differs between accounts and changes over time. A client built around a handful of values breaks the moment an admin adds one.

Duplicates are safe to retry

A create that collides with a record you already made returns 409 duplicate_value with the existing record's id attached:

{
"error": {
"code": "duplicate_value",
"message": "A record with these values already exists",
"existing_id": "cop-7Hs2Kd"
}
}

This is the intended mechanism for safe retries rather than an error to be surprised by. If your first attempt timed out but actually succeeded, the retry tells you so and hands you the id.

Handling errors in practice

ClassWhat to do
400 — any codeA bug in your request. Fix the payload; retrying unchanged will fail identically
401 invalid_credentialsStop and alert. Rotating or restoring the key is a human action; retrying will not help
403 access_deniedRead the message — it names the missing scope. Add it to the key
404 not_foundConfirm the id belongs to your account, and that you are not using an id from another environment
409 duplicate_valueTreat as success and adopt existing_id
409 immutable_state / inconsistent_stateRe-read the record; the state you assumed no longer holds
429 rate_limit_exceededSleep for Retry-After seconds, then retry. Do not retry sooner
5xxRetry with exponential backoff and a jitter

A minimal client that gets this right:

async function kaily(path, init = {}, attempt = 1) {
const response = await fetch(`https://api.kaily.ai/v1${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.KAILY_API_KEY}`,
'Content-Type': 'application/json',
...init.headers
}
});

if (response.ok) return response.json();

const body = await response.json().catch(() => ({ error: { code: 'server_error' } }));
const { code, message, existing_id: existingId } = body.error;

// A duplicate is not a failure: adopt the record we already created.
if (code === 'duplicate_value' && existingId) return { data: { id: existingId }, duplicate: true };

const retryable = response.status === 429 || response.status >= 500;
if (retryable && attempt < 5) {
const retryAfter = Number(response.headers.get('Retry-After'));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 250 + Math.random() * 250;
await new Promise(resolve => setTimeout(resolve, waitMs));
return kaily(path, init, attempt + 1);
}

throw new Error(`Kaily ${response.status} ${code}: ${message}`);
}
Kaily logo

More than just a virtual AI assistant, Kaily brings interactive, human-like conversations to your website. Easy to create, easier to customize, and the easiest to deploy—no code required. Let Kaily enhance your user experience using the information you provide.

Is this page useful?