Every non-2xx response from the API uses the same envelope. Branch on the HTTP status code and the 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
The error.type enum:
| Type | Status | When it fires |
|---|
authentication_error | 401 | Missing or invalid API 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. |
rate_limit_error | 429 | Too many requests. See Rate limits. |
api_error | 5xx | Server-side fault. Safe to retry idempotent calls with backoff. |
Additional type values (invalid_request_error, permission_error, idempotency_error) are reserved for endpoints that are not yet shipped.
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. |
conversation_not_found | not_found | The conversation does not exist, belongs to another organization, or has been archived. |
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. |
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 a Request-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.