<!-- נוצר אוטומטית ממקור התיעוד הקנוני של Teloring. אין לערוך קובץ זה. -->

עמוד קנוני: https://docs.teloring.com/he/api/guide-conventions
עודכן לאחרונה: 2026-08-20T20:49:57.000Z

# Pagination, filtering & errors

Learn these once and every endpoint behaves the way you expect.

## Responses

**A single resource is returned as itself.** No envelope, nothing to unwrap:

```json
{
  "object": "conversation",
  "id": "387",
  "status": "open",
  "…": "…"
}
```

Every resource carries an `object` field naming its type, which is what lets you
write one dispatcher over mixed results.

**A collection is `data` plus `meta`:**

```json
{
  "data": [ { "object": "conversation", "id": "387" } ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "count": 25,
    "total": 155,
    "total_pages": 7,
    "has_more": true
  }
}
```

**A delete answers uniformly**, so one handler covers all of them:

```json
{ "id": "387", "object": "conversation", "deleted": true }
```

Some deletes add a field describing the cascade — `unlinked_contacts` on a
customer, `detached_conversations` on a team, `freed_bytes` on a bulk file
delete. Read them; they are how you find out a delete did more than you expected.

## Pagination

| Parameter | Default | Range |
| --- | --- | --- |
| `page` | 1 | ≥ 1 |
| `per_page` | 25 | 1–100 |

`per_page=500` is a `400`, not a silent clamp. A client asking for 500 rows a
page has a design assumption worth correcting now rather than discovering in
production.

**Loop on `has_more`, not on `total`.** `total` is omitted where counting the
full result set would cost a second scan; `has_more` is always present.

```python
page = 1
while True:
    result = client.get("/conversations", status="open", page=page, per_page=100)
    for conversation in result["data"]:
        handle(conversation)
    if not result["meta"]["has_more"]:
        break
    page += 1
```

:::tip Polling for changes
Do not re-walk a whole collection on a timer. `GET /v1/conversations` takes
`updated_after`, so a poll can ask only for what moved:

```
GET /v1/conversations?status=open&updated_after=2026-08-20T09:00:00Z
```
:::

## Filtering

**Repeated values within one filter are OR.** Both forms work, so use whichever
your HTTP client produces:

```
?label=vip&label=urgent
?label=vip,urgent
```

**Different filters are AND.** This finds open, urgent conversations in two
inboxes that nobody owns:

```
?status=open&priority=urgent&inbox_id=3,7&assignee_id=unassigned
```

**Two filters take sentinel values:** `assignee_id=unassigned` for the waiting
line, and `team_id=none` for conversations no team owns.

**Timestamps are ISO-8601.** `2026-08-20T09:00:00Z` or with an offset. A value
that will not parse is a `400` naming the parameter rather than a silently
ignored filter.

**Unknown enum values are rejected,** and the error lists the accepted ones —
`?status=nope` tells you it wants `open`, `pending`, `on_hold` or `resolved`.
A filter you mistyped never silently returns everything.

## Errors

One shape, everywhere:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_parameter",
    "message": "'inbox_id' is required.",
    "param": "inbox_id",
    "request_id": "req_5f2a91c0e8b74d3a9c1e"
  }
}
```

Branch on **`code`** — it is stable across releases. `type` groups failures the
way you would handle them; `message` is written for a human reading a log.

### Status codes

| Status | `type` | Means |
| --- | --- | --- |
| 400 | `invalid_request_error` | Something about the request is wrong. Fix it; retrying will not help. |
| 401 | `authentication_error` | No token, or the token is no longer valid. Fetch a new one and retry once. |
| 402 | `plan_limit_error` | Valid and authorised, but a plan ceiling or a locked feature stopped it. |
| 403 | `permission_error` | The credential lacks the scope, an account switch is off, or the caller's IP is not on the account's allow-list. |
| 404 | `not_found_error` | No such resource in this account. |
| 409 | `conflict_error` | Valid, but conflicts with current state — a closed WhatsApp window, a duplicate team name. |
| 413 | `invalid_request_error` | The upload is too large. |
| 429 | `rate_limit_error` | Slow down. |
| 5xx | `api_error` | Our problem. Retry with backoff and quote the `request_id`. |

### Validation detail

Where several fields failed at once, the specifics arrive in `details`:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "One or more attribute values were rejected.",
    "details": {
      "errors": [
        "reason_for_contact: 'refund' is not one of the allowed options",
        "order_number: required"
      ]
    }
  }
}
```

### `request_id`

Every response carries one, in the body on errors and in the `X-Request-Id`
header always. Quote it when reporting a problem — it is how we find the exact
request in our logs.

You may also supply your own, and it will be echoed back and used in our logs,
which makes correlating with your side trivial:

```
X-Request-Id: my-job-4192-attempt-1
```

## Rate limits

| | Limit |
| --- | --- |
| Authenticated requests | 600 per minute, per credential |
| `POST /v1/oauth/token` | 20 per minute, per IP |

Over the limit is a `429`. Back off exponentially — and if you are hitting the
token limit, the fix is to cache the token rather than to retry harder.

## Retries and idempotency

Most write endpoints are not idempotent: `POST /v1/customers` twice makes two
customers. Where a retry is genuinely safe, it is because the endpoint was built
that way and says so:

- **`POST /v1/signature/links`** returns the existing link for the same document
  and contact instead of creating a second one, and does not charge again.
  `created` in the response says which happened.
- **`POST /v1/agents`** rolls the agent back if the seat charge is declined, so a
  `402` means nothing was created.
- **`PATCH`** endpoints are naturally idempotent — sending the same body twice
  leaves the same state.

For everything else, retry a `429` or a `5xx`; do not blind-retry a `400`.

## Timestamps and ids

All timestamps are **ISO-8601 in UTC**, and a field that has no value is `null`
rather than `""` — so you can tell "never resolved" from "resolved at an unknown
time".

Ids are **opaque strings**. Some look like numbers (`"387"`), some like hashes
(`"kIlBtSS5yQTGeghZmlQ3"`). Store them as strings; never parse or generate one.

## What this API will not do

Worth knowing before you plan around it:

- **No webhooks in this version.** Poll with `updated_after`. Studio can already
  call your endpoint on conversation events if you need push today.
- **No visual editors.** Studio flows, form layouts and signature field
  placement are coordinates on a canvas, and a JSON body of pixel offsets is not
  a contract anybody should have to write. Those stay in the console; this API
  reads them.
- **No browser access.** No CORS headers, deliberately.
