# Conventions

> Request format, common fields, list queries and filters, errors and rate limits shared by all endpoints.

This page describes the rules every endpoint of the integration API follows. The
endpoint pages only mention where an endpoint differs from them.

## Requests

The API is RPC over HTTP `POST`, not REST. Endpoints are `POST` requests to a path
that names the action, for example:

| Path | Action |
|---|---|
| `/api/v3/conversation/find` | list conversations |
| `/api/v3/conversation/get` | read one conversation |
| `/api/v3/conversation/delete` | delete one conversation |

There are no `PUT`, `PATCH` or `DELETE` endpoints and no resource ids in the path; the
id of a record is a parameter. Every endpoint accepts `POST`. Two read-only endpoints
without parameters, [credit balance](account.md#get-the-credit-balance) and
[version](account.md#get-the-server-version), also answer `GET` for compatibility with
older clients; use `POST` in new code.

The base URL is:

```text
https://ayeto.ai/api/v3
```

### Headers

| Header | Required | Description |
|---|---|---|
| `uni-api-key` | yes | Your API key, see [Authentication](authentication.md). |
| `Content-Type` | yes, with a body | `application/json`. |
| `language` | no | Language of the request: `EN`, `CZ` or `FR` (case-insensitive). Default `EN`. |
| `theme` | no | `light` or `dark`. Default `light`. Passed to the model in [chat](chat.md) as a hint about where the answer is displayed. |

The `language` header tells the model in [chat](chat.md) which language the user
writes in and selects the language of translated texts some endpoints return (for
example tool names in [models and tools](models-and-tools.md)). Error messages are
always in English.

The header applies to that request only. It never changes the language the user
chose in the AYETO app, which AYETO uses for the user's e-mails and scheduled work.

### Parameters

Parameters go in the JSON body unless the endpoint page says otherwise. Endpoints that
act on a single record by id (`.../get`, `.../delete`) take the id in the body as
`{"id": "<uuid>"}`:

```bash
curl -X POST "https://ayeto.ai/api/v3/conversation/get" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90"}'
```

They also accept the id as the `entity_id` query parameter, as earlier versions of the
API required; the body can then be left out or be `{}`. A body you do send must be JSON
with `Content-Type: application/json`, as for every endpoint:

```bash
curl -X POST "https://ayeto.ai/api/v3/conversation/get?entity_id=0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90" \
  -H "uni-api-key: $AYETO_API_KEY"
```

A request with neither, with both set to different ids, or with an `id` that is not a
UUID is rejected with `422`.

An endpoint whose parameters are a JSON object needs a body even when you want all
the defaults: send `{}`. A request without a body is rejected with `422`.

### Values

- **Ids** are UUIDs, written as strings: `"0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90"`.
  Responses always use the lower-case hyphenated form.
- **Timestamps** are integers: milliseconds since the Unix epoch (UTC), for example
  `1790846045000` for 2026-10-01 09:14:05 UTC. `0` means "never".

### Responses

A successful request returns `200` with a JSON body: an object, an array of objects
(`find` endpoints), a number (`count` endpoints) or the id of the affected record as a
JSON string (`delete` endpoints). [Streaming](#streaming) endpoints return a stream
instead. Errors are described under [Errors](#errors).

## Common fields

Every stored record the API returns (a conversation, an assistant, a workflow, a
workflow run, ...) carries its id and three metadata objects:

| Field | Type | Description |
|---|---|---|
| `id` | UUID | Id of the record. |
| `created` | object | When and by whom the record was created. |
| `updated` | object | When and by whom it was last changed. `timestamp` is `0` if it has never been changed. |
| `accessed` | object | When and by whom it was last written or opened. Reading some records (for example a conversation through `conversation/get`) updates it, at most about once a minute. |

Each metadata object has the same shape:

| Field | Type | Description |
|---|---|---|
| `timestamp` | timestamp | Time of the event, `0` if it has not happened. |
| `user_id` | UUID or `null` | User who caused it; `null` when it was done by the system. |

```json
{
  "id": "0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90",
  "created": { "timestamp": 1790846045000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" },
  "updated": { "timestamp": 1790846052000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" },
  "accessed": { "timestamp": 1790846052000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" }
}
```

Most endpoints return a public view of the record: these fields plus the fields listed
on the endpoint page. Endpoints that return the full record (for example
[workflows](workflows.md) and workflow runs) also include these general fields:

| Field | Type | Description |
|---|---|---|
| `owner` | UUID or `null` | General-purpose reference, usually `null`. |
| `parent` | UUID or `null` | General-purpose reference, usually `null`. |
| `source` | UUID or `null` | Record this one belongs to, when it has one. |
| `seq` | integer | Sequence number of the record within its type. |
| `enabled` | boolean | Usually `true`. |
| `note` | string | Free-text note, usually empty. |
| `owner_group` | UUID or `null` | Group that owns the record (for example the administrators of an organization); `null` when it is owned by the user who created it. |
| `permissions` | object | Access flags `group`, `all` and `other`, each `{ "read": boolean, "write": boolean }`. |
| `joined_collections` | `null` | Always `null` in this API. |

`created`, `updated`, `accessed`, `permissions` and `owner_group` are managed by the
server. Values you send for them in create or update requests are ignored.

## Querying lists

Endpoints that list records (`.../find`) and count them (`.../count`) take the same
query object as their body. The running example is
`POST /api/v3/conversation/find`.

| Field | Type | Required | Description |
|---|---|---|---|
| `filters` | array | no | Conditions the records must match, see [Filters](#filters). All conditions in the array must match. |
| `sort_key` | string | no | Field to sort by, for example `"name"` or `"updated.timestamp"` (dot notation for nested fields). It must be a field of the record; an unknown field is rejected with `422`. |
| `sort_order` | integer | no | `0` = ascending (default when `sort_key` is set), `1` = descending. Ignored without `sort_key`. |
| `limit_from` | integer | no | Index of the first record to return, `0`-based. Paging is applied only when this field is set. |
| `limit_to` | integer | no | Index **after** the last record to return. This is an absolute position, not a page size: `limit_from: 20, limit_to: 40` returns records 20 to 39. `null` or `0` means "to the end". |
| `fetch_dict` | boolean | no | Has no effect on the returned data; you can omit it. |
| `join` | array | no | Not supported by the integration API; ignored. |

Without `sort_key`, records are returned newest first (by `created.timestamp`,
descending). Without `limit_from`, all matching records are returned in one response.

### Paging

To read page *n* (counting from 0) of size *s*, send `limit_from: n * s` and
`limit_to: (n + 1) * s`. Rules:

- `limit_from` must be `0` or more and `limit_to` must be greater than `limit_from`.
  A negative `limit_from` or a `limit_to` lower than `limit_from` is rejected with
  `422`. When the two are equal, no upper bound is applied and every record from
  `limit_from` on is returned.
- `limit_to` without `limit_from` is ignored: the whole result is returned.
- There is no server-side maximum page size. Keep pages reasonably small (up to a few
  hundred records); large records such as conversations with long histories make big
  responses.

```bash
curl -X POST "https://ayeto.ai/api/v3/conversation/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [["updated.timestamp", ">=", 1790812800000]],
    "sort_key": "updated.timestamp",
    "sort_order": 1,
    "limit_from": 0,
    "limit_to": 20
  }'
```

The response is an array of records; an empty array when nothing matches:

```json
[
  {
    "id": "0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90",
    "name": "Quarterly report outline",
    "model": "gpt-5-mini",
    "summary": "",
    "organization_id": null,
    "assistant_id": "c4e2a9b1-7f3d-4e6a-8b5c-1d9f0a2e3b47",
    "task_id": null,
    "task_execution_id": null,
    "workflow_id": null,
    "workflow_run_id": null,
    "messages": [
      {
        "id": "7d3a1f9e-2b4c-4e8a-9f6d-0c5b8e1a2d36",
        "timestamp": 1790846045000,
        "role": "user",
        "content": "Draft an outline for the Q3 report.",
        "reasoning_content": null,
        "model": null,
        "attachments": [],
        "tool_runs": {}
      },
      {
        "id": "e1b9c7a3-5d2f-4a6e-8c0b-9f4d3e2a1b58",
        "timestamp": 1790846052000,
        "role": "assistant",
        "content": "1. Summary\n2. Revenue\n3. Costs\n4. Outlook",
        "reasoning_content": null,
        "model": "gpt-5-mini",
        "attachments": [],
        "tool_runs": {}
      }
    ],
    "public_sharing": false,
    "created": { "timestamp": 1790846045000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" },
    "updated": { "timestamp": 1790846052000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" },
    "accessed": { "timestamp": 1790846052000, "user_id": "5a1d7e3c-9b2f-4c8e-a6d0-3f7b1e9c2a54" }
  }
]
```

To read everything, request pages until one comes back shorter than the page size.
Sort ascending by `created.timestamp`, so that records created while you page are
appended at the end instead of shifting the pages you have not read yet:

```python
import os
import requests

BASE = "https://ayeto.ai/api/v3"
HEADERS = {"uni-api-key": os.environ["AYETO_API_KEY"]}
PAGE = 50

start = 0
while True:
    r = requests.post(f"{BASE}/conversation/find", headers=HEADERS, json={
        "sort_key": "created.timestamp",
        "sort_order": 0,
        "limit_from": start,
        "limit_to": start + PAGE,
    })
    r.raise_for_status()
    page = r.json()
    for conversation in page:
        print(conversation["id"], conversation["name"])
    if len(page) < PAGE:
        break
    start += PAGE
```

### Counting

`.../count` endpoints take the same body and return the number of records that match
`filters` as a plain integer. They ignore `limit_from`, `limit_to` and the sort
fields, so you can send the same body as for the page you display and get the total
for the pager.

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/count" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": [["name", "regex", "invoice"]], "limit_from": 0, "limit_to": 20}'
```

```json
42
```

Not every list has a count endpoint; the endpoint pages list the ones that exist.

## Filters

`filters` is an array. Each element is a condition or a group of conditions, and a
record is returned only when it matches **all** elements.

### Conditions

A condition is an array `[field, operator, value]`, with an optional fourth element
(see [UUID conversion](#uuid-conversion)):

```json
["model", "==", "gpt-5-mini"]
```

`field` names a field of the record. Use dot notation for nested fields
(`created.timestamp`, `created.user_id`). A path into a list of objects matches when
any element of the list matches (`messages.role`). `id` is the record id.

| Operator | Matches when the field... | Value |
|---|---|---|
| `==` | equals the value | any allowed value |
| `!=` | does not equal the value | any allowed value |
| `<` | is less than the value | number or string |
| `>` | is greater than the value | number or string |
| `<=` | is less than or equal to the value | number or string |
| `>=` | is greater than or equal to the value | number or string |
| `regex` | contains a match of the regular expression, ignoring case | string |
| `in` | equals any of the values | non-empty array, at most 100 values |

Notes:

- `regex` is always case-insensitive and not anchored: `"invoice"` matches
  `"Invoices 2026"`. Use `^` and `$` inside the pattern to anchor it, and escape regular
  expression characters (`.`, `*`, `+`, `?`, `(`, `)`, `[`, `]`, `\`) to match them
  literally.
- Strings compare alphabetically (by character code), numbers numerically. Compare
  timestamps as numbers.
- `["field", "==", null]` matches records where the field is `null` or missing.

### Groups

A group is an object with exactly one key, `AND` or `OR`, whose value is a non-empty
array of conditions or further groups. Groups can be nested.

```json
{"OR": [["model", "==", "gpt-5-mini"], ["model", "==", "gpt-5"]]}
```

```json
{"AND": [
  ["updated.timestamp", ">=", 1788220800000],
  {"OR": [["name", "regex", "report"], ["summary", "regex", "report"]]}
]}
```

The key must be written in upper case. **An object with any other key (for example
`"or"`) is ignored and matches every record**, so a typo silently disables that part
of the filter. An object with no key or more than one key, and a group whose value is
not a non-empty array, are rejected with `422`. Groups can be nested up to 32 levels.

### Examples

```json
{
  "filters": [
    ["assistant_id", "==", "c4e2a9b1-7f3d-4e6a-8b5c-1d9f0a2e3b47"],
    ["created.timestamp", ">=", 1788220800000],
    ["name", "regex", "^draft"]
  ]
}
```

```json
{
  "filters": [
    ["id", "in", [
      "0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90",
      "9e7a5c3b-1d2f-4b6a-8e0c-4f3d2a1b9c87"
    ]]
  ]
}
```

```json
{
  "filters": [
    {"OR": [["organization_id", "==", null], ["organization_id", "==", "2f9c4b7e-8a1d-4e3c-b6f0-5d7a9e2c1b34"]]}
  ]
}
```

The server checks and normalises every condition before it runs the query. Most
surprising empty results come from one of the rules below.

### UUID conversion

A string value that is a valid UUID is converted to a UUID before matching. Fields that
hold ids (`id`, `assistant_id`, `created.user_id`, ...) are stored as UUIDs, so this
is what you want for them. A field that holds **text** which happens to look like a UUID
(including 32 hexadecimal digits without hyphens) will not match after the conversion.
Add `true` as the fourth element to compare the value as plain text:

```json
["external_ref", "==", "0b8f3c2e5d414a7e9c1f6e2d8a4b7c90", true]
```

With `in`, the fourth element applies to every value in the list. Values inside an
array used with `==` or `!=` are never converted.

### HTML escaping

In string values, `<`, `>`, `"` and `'` are replaced with `&lt;`, `&gt;`, `&quot;` and
`&#x27;` before matching. A filter for text that contains these characters therefore
does not match records stored with the plain characters; filter on a part of the text
without them, for example with `regex`.

### Value limits

| Value | Rule |
|---|---|
| string | At most 1,000 characters. Must not start with `$`. Null characters are removed. |
| array | At most 100 elements (also for `in`). Each element must be an allowed value. `in` needs at least one element. |
| allowed types | string, number, boolean, `null`, array. An object as a value is rejected. |

### Field names

A field name must start with a letter and contain only letters, digits, `_` and `.`
(at most 128 characters). Other names are rejected with `422`.

Field names in filters are not checked against the record: a misspelled field matches
nothing with `==` and everything with `!=`. (`sort_key`, by contrast, is checked.)

### Malformed conditions

A condition with fewer than three or more than four elements, or an element of
`filters` (or of a group) that is neither an array nor an object, is rejected with
`422`. `detail` says what is wrong, for example
`A filter condition must be [field, operator, value] with an optional fourth element, got 2 elements`.

## Errors

Errors use standard HTTP status codes and a JSON body with a `detail` field:

```json
{"detail": "entity not found, id: 0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90"}
```

`detail` is a human-readable English message. Do not parse it, except where an endpoint
page documents a specific message.

When the request itself does not match the endpoint's schema (a missing or wrongly
typed field, an invalid UUID, a missing header, no body), the status is `422` and
`detail` is a list that points at each problem:

```json
{
  "detail": [
    {
      "loc": ["body", "sort_order"],
      "msg": "value is not a valid enumeration member; permitted: 0, 1",
      "type": "type_error.enum",
      "ctx": {"enum_values": [0, 1]}
    }
  ]
}
```

`loc` is the location of the problem: `body`, `query` or `header`, followed by the
field path.

One response differs from this shape: an unexpected server failure can return `500`
with the plain-text body `Internal Server Error`.

Read the status code first and treat the body as optional information.

| Status | Meaning | What to do |
|---|---|---|
| `401` | The API key is empty, unknown, deleted, disabled or lacks a scope the endpoint requires. `detail` is `API key not provided` or `API key is invalid`. | Check the key and its [scopes](authentication.md#scopes). Do not retry unchanged. |
| `403` | The key is valid, but its user may not do this: the record belongs to someone else, or the account lacks the permission. `detail` is usually `permission denied`. | Do not retry. |
| `404` | The record does not exist (or was deleted). | Do not retry. |
| `422` | The request is invalid: schema errors (list `detail`, see above), a rejected filter or sort key, or a business rule. Running out of credits is also `422`, with `detail` `not enough user credit` or `not enough organization credit`. | Fix the request, or top up credits. |
| `423` | The resource is locked by another operation (for example a [booster database](booster-database.md) record). | Retry after a short pause. |
| `429` | Rate limit exceeded, see [Rate limits](#rate-limits). | Wait for `Retry-After` seconds. |
| `500` | Server error. | Retry later with backoff; report it if it persists. |
| `502`, `503`, `504`, `529` | An AI provider or another upstream service failed or is overloaded. | Retry later with backoff. |

The API does not use `402`; credit errors are `422` as described above. A missing
`uni-api-key` header (as opposed to an empty or wrong value) is a schema error and
returns `422`, not `401`.

Responses to failed API key checks (`401`) and to rate-limited requests (`429`) are
delayed by about one second on purpose, to slow down key guessing. Account for this
in your timeouts and do not treat the delay as a server problem.

Errors that happen after a [stream](streaming.md) has started are reported inside the
stream, not as an HTTP status.

## Rate limits

Requests are rate limited per client IP address and per endpoint: every endpoint path
has its own counters, and all requests from one IP address to that endpoint count
together, whatever API key they use. Requests count even when they fail, including
requests rejected for a bad API key.

Each endpoint has three limits checked at the same time: per minute, per hour and per
day. They are moving windows: a request counts against a window for the full length of
that window after it was made, so capacity frees up gradually rather than at the top
of the minute or hour. Every endpoint page states its tier:

| Tier | Per minute | Per hour | Per day |
|---|---|---|---|
| low | 10 | 100 | 1,000 |
| default | 60 | 2,000 | 20,000 |
| high | 120 | 4,000 | 40,000 |
| static | 1,200 | 30,000 | 300,000 |
| webhook | 6,000 | 120,000 | 1,500,000 |

The webhook tier applies only to the public [workflow webhook](workflows.md), which
additionally limits each trigger on its own. The static tier is a flood guard for
endpoints that are polled often, such as [the server version](account.md#get-the-server-version).

When a limit is exceeded, the API returns `429` with a `Retry-After` header: the number
of whole seconds until the window has room for another request.

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 17
Content-Type: application/json

{"detail": "Too many requests"}
```

Successful responses carry no rate-limit headers, so keep track of your own request
rate. To stay within the limits:

- Wait at least `Retry-After` seconds before retrying; never retry a `429` in a tight
  loop.
- For repeated failures (`429`, `5xx`), back off exponentially with some random jitter.
- Spread batch work over time instead of sending it in bursts, and cache lists you read
  often instead of fetching them on every use.
- Several workers behind one IP address share the limits; budget for all of them.

## Streaming

Chat and some workflow endpoints can stream their output: the response is sent in
chunks while it is being generated, instead of as one JSON body at the end. The chunk
format, the end-of-stream signal and how errors are reported mid-stream are described
in [Streaming](streaming.md).
