Pagination, filtering, errors and rate limits
How lists page, how requests are refused, what each status code means, and what to do when you are throttled.
Last revised: 2026-08-23
Pagination
Every list endpoint takes page and page_size and returns the same envelope. total is the count of everything matching the filters, not the size of this page - so Math.ceil(total / page_size) is the number of pages.
| Parameter | Default | Range |
|---|---|---|
page | 1 | 1 and up |
page_size | 25 | 1–200 (larger values are clamped, not rejected) |
curl -sS "https://your-host/api/v1/crm/contacts?page=2&page_size=100" \
-H "Authorization: Bearer $CLIENTO_TOKEN"{
"items": [ { "id": 4182, "display_name": "Sara Ahmadi", "…": "…" } ],
"page": 2,
"page_size": 100,
"total": 3417
}Filtering and search
q is free-text search across the fields a person would search - names, email, company. Everything else is an explicit query parameter, and each collection documents its own set in the OpenAPI document.
# Free-text
curl -sS "https://your-host/api/v1/crm/contacts?q=ahmadi" \
-H "Authorization: Bearer $CLIENTO_TOKEN"
# Typed filters (tasks)
curl -sS "https://your-host/api/v1/crm/tasks?status=open&priority=high&overdue=true" \
-H "Authorization: Bearer $CLIENTO_TOKEN"An unknown query parameter is ignored rather than rejected - so a typo returns unfiltered results rather than an error. Check the parameter names against the OpenAPI document when a filter appears to do nothing.
The error envelope
Every refusal, from a malformed body to an internal failure, returns the same three fields. Nothing returns an HTML error page, so a client never has to guess whether a body is parseable.
{
"error": "validation_error",
"message": "Invalid payload",
"details": { "email": ["Not a valid email address."] }
}- `error` - a stable machine code. Branch on this, and localize on this.
- `message` - English diagnostic text for a developer. Do not show it to an end user and do not match on it; the wording may change.
- `details` - structure that varies by error: field errors for validation,
requiredfor a permission refusal,reasonfor a business rule,retry_afterfor a throttle. May be null.
Status codes
| Code | error | Meaning and what to do |
|---|---|---|
| 200 | - | Read or update succeeded. |
| 201 | - | Created. The body is the new record. |
| 204 | - | Deleted. No body. |
| 400 | validation_error or a rule code | The payload is wrong. details names the fields; a broken business rule (for example payment_amount_not_positive) has its own code. Do not retry unchanged. |
| 401 | unauthorized | Missing, expired or revoked credential. Refresh once, then stop. |
| 403 | forbidden | Authenticated but not permitted. details.required names the codes. |
| 404 | not_found | No such record - or one you may not see. The two are deliberately the same answer. |
| 409 | conflict or a rule code | A uniqueness or state rule. Finance, sales, CRM and ticket rules carry their own code (for example invoice_has_payments, payment_exceeds_due) with the numbers in details; elsewhere details.reason says which. Branch on the status if you do not recognise the code. |
| 429 | rate_limited | Throttled. Wait for Retry-After, then retry. |
| 5xx | internal_error | A fault on our side. Retry with backoff; the request may or may not have applied. |
Rate limits
Sensitive endpoints - sign-in, the customer portal's one-time codes, public form submissions, public invoice pages and payment callbacks - are throttled per caller. Ordinary authenticated CRUD is not throttled by default, but a workspace's operator may configure limits, so a well-behaved client handles 429 regardless.
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{
"error": "rate_limited",
"message": "Too many requests",
"details": { "retry_after": 42 }
}- Honour
Retry-After(seconds). It is the server's own answer - a shorter wait is simply refused again. - Back off exponentially with jitter on 5xx. A fixed retry interval turns a brief outage into a stampede at recovery.
- Retry
GETandDELETEfreely - they are idempotent. ForPOST, see the next section.
Idempotency
There is no global Idempotency-Key header. Where a duplicate would be harmful, the endpoint carries its own idempotency identity in the body, and it is required rather than optional - the client owns the identity, because only the client knows that a retry is a retry.
| Endpoint | Identity | Behaviour on repeat |
|---|---|---|
POST /chat/conversations/{id}/messages | client_message_id | Returns the existing message with 200 instead of 201 |
POST /chat/conversations/support | The caller | Resolves to the same conversation |
| Webhook delivery (inbound to you) | delivery_id | See the webhooks guide - deliveries are at-least-once |
For endpoints without an idempotency field, a POST that times out has an unknown outcome. Do not blind-retry: read back with a GET filtered on something you control - an external reference you stored on the record - and create only if it is genuinely absent.