Reference
Errors and limits
Problem details, the status codes you will meet, rate limits, idempotency, pagination bounds and how to handle each in code.
Last updated
Error shape
Errors follow RFC 9457 problem details.
{
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"errors": [ { "location": "body.inbox_id", "message": "required property is missing" } ]
}| Status | Meaning | Do |
|---|---|---|
| 400 | Malformed request or JSON. | Fix the request; do not retry as is. |
| 401 | Missing, invalid, expired or revoked credential. | Re-authenticate; check the key was not revoked. |
| 403 | Valid credential without the permission; detail names it. | Add the permission to the key, or use another credential. |
| 404 | Not found, or not visible to this caller. | Ids from another workspace look like 404 by design. |
| 409 | Conflict: duplicate identity, app already installed, stale update. | Read the existing object and update instead. |
| 422 | Validation, a plugin or app veto (reason in detail), or a search that was too broad. | Show the reason; narrow the request. |
| 429 | Rate limit; Retry-After in seconds. | Wait and retry with backoff. |
| 5xx | Server error. | Retry with the same Idempotency-Key. |
A retry helper
export async function call(input: RequestInfo, init: RequestInit = {}, attempt = 0): Promise<Response> {
const res = await fetch(input, init);
if ((res.status === 429 || res.status >= 500) && attempt < 5) {
const wait = Number(res.headers.get("retry-after")) * 1000 || Math.min(30_000, 500 * 2 ** attempt);
await new Promise((r) => setTimeout(r, wait));
return call(input, init, attempt + 1); // safe: writes carry an Idempotency-Key
}
return res;
}import time, requests
def call(method, url, attempt=0, **kw):
r = requests.request(method, url, **kw)
if r.status_code in (429,) or r.status_code >= 500:
if attempt < 5:
time.sleep(float(r.headers.get("Retry-After") or min(30, 0.5 * 2 ** attempt)))
return call(method, url, attempt + 1, **kw) # safe with an Idempotency-Key
return rLimits
| Limit | Value |
|---|---|
| Requests per minute per API key | 600 |
| Requests per minute per workspace | 3000 |
| Requests per minute per IP, unauthenticated | 300, stricter on login and password reset |
| List page size | limit up to 100; search up to 50 |
| Message content | 20 000 characters |
| Idempotency window | 24 hours; same key with a different body is 422 |
| Synchronous app hook timeout | 3 s default, 10 s maximum |
| Webhook retries | After 1 min, 5 min, 15 min, 1 h, 4 h, 8 h and 12 h, then failed |
| Bot delivery retries | After 30 s, 2 min and 10 min, then failed |
| Bot typing indicator | 20 a minute per conversation |
| Rule runs per conversation | max_runs_per_hour, default 20 (1 to 100) |
| "Replay all failed" | The oldest 100 per call, once a minute per webhook, bot or rule |
| Manifest size | 256 KiB |
Redaction
Fields the caller may not see are masked or omitted and listed under _redacted, for example ["items.3.emails", "items.3.phones"]. Treat a listed path as "exists but hidden", not as empty.
Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].