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 and
version, also answer GET for compatibility with
older clients; use POST in new code.
The base URL is:
https://ayeto.ai/api/v3
Headers
| Header | Required | Description |
|---|---|---|
uni-api-key |
yes | Your API key, see Authentication. |
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 as a hint about where the answer is displayed. |
The language header tells the model in chat which language the user
writes in and selects the language of translated texts some endpoints return (for
example tool names in models and tools). 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>"}:
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:
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
1790846045000for 2026-10-01 09:14:05 UTC.0means "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 endpoints return a stream
instead. Errors are described under 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. |
{
"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 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. 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_frommust be0or more andlimit_tomust be greater thanlimit_from. A negativelimit_fromor alimit_tolower thanlimit_fromis rejected with422. When the two are equal, no upper bound is applied and every record fromlimit_fromon is returned.limit_towithoutlimit_fromis 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.
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:
[
{
"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:
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.
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}'
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):
["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:
regexis 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 isnullor 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.
{"OR": [["model", "==", "gpt-5-mini"], ["model", "==", "gpt-5"]]}
{"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
{
"filters": [
["assistant_id", "==", "c4e2a9b1-7f3d-4e6a-8b5c-1d9f0a2e3b47"],
["created.timestamp", ">=", 1788220800000],
["name", "regex", "^draft"]
]
}
{
"filters": [
["id", "in", [
"0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90",
"9e7a5c3b-1d2f-4b6a-8e0c-4f3d2a1b9c87"
]]
]
}
{
"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:
["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 <, >, " and
' 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:
{"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:
{
"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. 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 record). | Retry after a short pause. |
429 |
Rate limit exceeded, see 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 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, 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.
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/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-Afterseconds before retrying; never retry a429in 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.