error.type enum — never on error.message, which is human-readable and may change.
Envelope
{
"error": {
"type": "validation_error",
"code": "invalid_query_parameter",
"message": "Number must be less than or equal to 100",
"param": "limit",
"docUrl": "https://docs.replyful.com/errors/invalid_query_parameter",
"requestId": "req_01HK7GD3H6ZSQ0Y2A5B3C4DEFG"
}
}
| Field | Type | Description |
|---|---|---|
type | string | High-level category. Stable enum — branch on this. |
code | string | Specific machine-readable reason. Stable — branch on this for fine-grained handling. |
message | string | Human-readable explanation. May change between releases. Do not parse. |
param | string | undefined | Dotted path to the offending field, when applicable. |
docUrl | string | Deep link to the docs page for this code. |
requestId | string | Echoes the response Request-Id header. Quote this in support tickets. |
Status code is the source of truth. The API will never return
200 OK with an error body. If your status check passes, the response is success.Error types
Theerror.type enum:
| Type | Status | When it fires |
|---|---|---|
invalid_request_error | 400, 409 | The request is malformed, has an unknown field, or conflicts with the current state. |
authentication_error | 401 | Missing or invalid API key. |
permission_error | 403 | The API key is not allowed to do this, such as writing with a read-only key. |
not_found | 404 | The resource does not exist or is not visible to your API key. |
validation_error | 422 | Request was well-formed but failed validation. |
idempotency_error | 409 | An Idempotency-Key was reused with a different request, or its first request is still running. |
rate_limit_error | 429 | Too many requests. See Rate limits. |
api_error | 5xx | Server-side fault. Safe to retry idempotent calls with backoff. |
Error codes shipped today
| Code | Type | Meaning |
|---|---|---|
missing_api_key | authentication_error | The Authorization header is absent or does not start with Bearer . |
invalid_api_key | authentication_error | The key is malformed, unknown, or archived. |
ticket_not_found | not_found | The ticket does not exist or belongs to another organization. |
conversation_not_found | not_found | The conversation does not exist or belongs to another organization. |
channel_not_found | not_found | The channel does not exist or belongs to another organization. |
contact_not_found | not_found | The contact does not exist, belongs to another organization, or has been erased. |
attachment_not_found | not_found | The attachment does not exist or belongs to another organization. |
invalid_query_parameter | validation_error | A query parameter failed validation. The param field names the offender. |
invalid_path_parameter | validation_error | A path parameter failed validation. The param field names the offender. |
invalid_cursor | validation_error | The startingAfter cursor could not be decoded. |
conflicting_date_range | validation_error | A request mixed createdAt[...] and updatedAt[...] filters. Pick one. |
invalid_json | invalid_request_error | The request body is not valid JSON. |
unknown_field | invalid_request_error | The body has a field the endpoint does not know. The param field names it. |
invalid_body | validation_error | A body field failed validation. The param field names the offender. |
invalid_channel | validation_error | channelId is not one of your chat channels. |
requester_not_found | validation_error | requester.id matches no contact. |
email_required | validation_error | A handover needs an email for the requester and none is known. |
no_email_channel | validation_error | A handover needs an email channel to reply from and you have none. |
handover_email_channel_not_set | validation_error | You have several email channels and the chat channel’s handover settings name none. |
ticket_not_on_chat_channel | validation_error | Only tickets that started on a chat channel can be handed over. |
conversation_not_writable | validation_error | Messages can only be added to the primary conversation of a ticket that started on a chat channel. |
idempotency_key_too_long | invalid_request_error | Idempotency-Key is longer than 255 characters. |
write_access_required | permission_error | The key is read-only. Use a Read & write key. |
test_mode_write_not_supported | permission_error | Test keys cannot write yet. |
idempotency_key_reused | idempotency_error | The Idempotency-Key was already used with a different request. |
idempotency_request_in_progress | idempotency_error | The first request with this Idempotency-Key is still running. Retry shortly. |
ticket_external_id_taken | invalid_request_error | A ticket with this externalId already exists. |
conversation_handed_over | invalid_request_error | A person handles the ticket now, so chatbot messages are refused. |
too_many_requests | rate_limit_error | Rate limit exceeded. Honor Retry-After. |
internal_server_error | api_error | Unexpected server-side fault. Retry with backoff. |
Examples
401 missing_api_key
{
"error": {
"type": "authentication_error",
"code": "missing_api_key",
"message": "Missing Authorization header. Provide `Authorization: Bearer rfl_…`.",
"docUrl": "https://docs.replyful.com/errors/missing_api_key",
"requestId": "req_..."
}
}
422 invalid_query_parameter
{
"error": {
"type": "validation_error",
"code": "invalid_query_parameter",
"message": "Number must be less than or equal to 100",
"param": "limit",
"docUrl": "https://docs.replyful.com/errors/invalid_query_parameter",
"requestId": "req_..."
}
}
422 conflicting_date_range
{
"error": {
"type": "validation_error",
"code": "conflicting_date_range",
"message": "Cannot filter by both createdAt and updatedAt date ranges.",
"docUrl": "https://docs.replyful.com/errors/conflicting_date_range",
"requestId": "req_..."
}
}
429 too_many_requests
{
"error": {
"type": "rate_limit_error",
"code": "too_many_requests",
"message": "Rate limit exceeded. Slow down and try again shortly.",
"docUrl": "https://docs.replyful.com/errors/too_many_requests",
"requestId": "req_..."
}
}
Handling errors
A pragmatic client:async function callReplyful(path: string, init?: RequestInit) {
const res = await fetch(`https://api.replyful.com${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.REPLYFUL_API_KEY}`,
...init?.headers,
},
});
if (res.ok) {
return res.json();
}
const body = await res.json().catch(() => ({}));
const requestId = res.headers.get("Request-Id") ?? body.error?.requestId;
// Branch on type for high-level handling.
switch (body.error?.type) {
case "authentication_error":
throw new Error(`Replyful auth failed (${requestId}). Check your API key.`);
case "rate_limit_error": {
const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
throw new RateLimitError(retryAfter, requestId);
}
case "validation_error":
throw new Error(`Validation failed: ${body.error.message} (${requestId})`);
default:
throw new Error(`Replyful API error (${requestId}): ${body.error?.message ?? res.statusText}`);
}
}
Request IDs
Every response carries aRequest-Id header (e.g. req_01HK7GD3H6ZSQ0Y2A5B3C4DEFG). The same value appears in the error body’s requestId field. We retain request traces for 90 days — quote the ID in support emails to get the exact context of a failing call.