Guides

Conventions

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

View as Markdown

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:

text
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>"}:

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 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.
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 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_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):

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. 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
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.