# Conventions

> Format des requêtes, champs communs, requêtes de listes et filtres, erreurs et limites de débit communs à tous les endpoints.

Cette page décrit les règles que suit chaque endpoint de l'API d'intégration. Les
pages des endpoints ne mentionnent que les cas où un endpoint s'en écarte.

## Requêtes

L'API est de type RPC sur HTTP `POST`, et non REST. Les endpoints sont des requêtes `POST` vers un chemin
qui nomme l'action, par exemple :

| Chemin | Action |
|---|---|
| `/api/v3/conversation/find` | lister les conversations |
| `/api/v3/conversation/get` | lire une conversation |
| `/api/v3/conversation/delete` | supprimer une conversation |

Il n'existe aucun endpoint `PUT`, `PATCH` ou `DELETE` ni aucun identifiant de ressource dans le chemin ;
l'identifiant d'un enregistrement est un paramètre. Chaque endpoint accepte `POST`. Deux endpoints en lecture seule
sans paramètres, [solde de crédits](account.md#obtenir-le-solde-de-credits) et
[version](account.md#obtenir-la-version-du-serveur), répondent aussi à `GET` pour la compatibilité avec
les anciens clients ; utilisez `POST` dans le nouveau code.

L'URL de base est :

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

### En-têtes

| En-tête | Obligatoire | Description |
|---|---|---|
| `uni-api-key` | oui | Votre clé API, voir [Authentification](authentication.md). |
| `Content-Type` | oui, avec un corps | `application/json`. |
| `language` | non | Langue de la requête : `EN`, `CZ` ou `FR` (insensible à la casse). Par défaut `EN`. |
| `theme` | non | `light` ou `dark`. Par défaut `light`. Transmis au modèle dans le [chat](chat.md) comme indication sur l'endroit où la réponse est affichée. |

L'en-tête `language` indique au modèle dans le [chat](chat.md) la langue dans laquelle l'utilisateur
écrit et sélectionne la langue des textes traduits que renvoient certains endpoints (par
exemple les noms des outils dans [modèles et outils](models-and-tools.md)). Les messages d'erreur sont
toujours en anglais.

L'en-tête ne s'applique qu'à cette requête. Il ne modifie jamais la langue que l'utilisateur
a choisie dans l'application AYETO, et qu'AYETO utilise pour les e-mails de l'utilisateur et les tâches planifiées.

### Paramètres

Les paramètres sont envoyés dans le corps JSON, sauf indication contraire sur la page de l'endpoint. Les endpoints qui
agissent sur un seul enregistrement par identifiant (`.../get`, `.../delete`) prennent l'identifiant dans le corps sous la forme
`{"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"}'
```

Ils acceptent aussi l'identifiant dans le paramètre de requête `entity_id`, comme l'exigeaient les versions antérieures de
l'API ; le corps peut alors être omis ou valoir `{}`. Un corps que vous envoyez doit être du JSON
avec `Content-Type: application/json`, comme pour tout 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"
```

Une requête sans aucun des deux, avec les deux définis sur des identifiants différents, ou avec un `id` qui n'est pas un
UUID est rejetée avec `422`.

Un endpoint dont les paramètres sont un objet JSON exige un corps, même si vous voulez toutes
les valeurs par défaut : envoyez `{}`. Une requête sans corps est rejetée avec `422`.

### Valeurs

- Les **identifiants** sont des UUID, écrits sous forme de chaînes : `"0b8f3c2e-5d41-4a7e-9c1f-6e2d8a4b7c90"`.
  Les réponses utilisent toujours la forme en minuscules avec tirets.
- Les **horodatages** sont des entiers : des millisecondes depuis l'epoch Unix (UTC), par exemple
  `1790846045000` pour 2026-10-01 09:14:05 UTC. `0` signifie « jamais ».

### Réponses

Une requête réussie renvoie `200` avec un corps JSON : un objet, un tableau d'objets
(endpoints `find`), un nombre (endpoints `count`) ou l'identifiant de l'enregistrement concerné sous forme de
chaîne JSON (endpoints `delete`). Les endpoints en [streaming](#streaming) renvoient un stream
à la place. Les erreurs sont décrites dans [Erreurs](#erreurs).

## Champs communs

Chaque enregistrement stocké que renvoie l'API (une conversation, un assistant, un workflow, une
exécution de workflow, ...) porte son identifiant et trois objets de métadonnées :

| Champ | Type | Description |
|---|---|---|
| `id` | UUID | Identifiant de l'enregistrement. |
| `created` | objet | Quand et par qui l'enregistrement a été créé. |
| `updated` | objet | Quand et par qui il a été modifié pour la dernière fois. `timestamp` vaut `0` s'il n'a jamais été modifié. |
| `accessed` | objet | Quand et par qui il a été écrit ou ouvert pour la dernière fois. La lecture de certains enregistrements (par exemple une conversation via `conversation/get`) le met à jour, au plus environ une fois par minute. |

Chaque objet de métadonnées a la même forme :

| Champ | Type | Description |
|---|---|---|
| `timestamp` | horodatage | Moment de l'événement, `0` s'il n'a pas eu lieu. |
| `user_id` | UUID ou `null` | Utilisateur qui l'a provoqué ; `null` lorsqu'il a été effectué par le système. |

```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" }
}
```

La plupart des endpoints renvoient une vue publique de l'enregistrement : ces champs plus les champs listés
sur la page de l'endpoint. Les endpoints qui renvoient l'enregistrement complet (par exemple
les [workflows](workflows.md) et les exécutions de workflow) incluent aussi ces champs généraux :

| Champ | Type | Description |
|---|---|---|
| `owner` | UUID ou `null` | Référence à usage général, généralement `null`. |
| `parent` | UUID ou `null` | Référence à usage général, généralement `null`. |
| `source` | UUID ou `null` | Enregistrement auquel celui-ci appartient, s'il y en a un. |
| `seq` | entier | Numéro de séquence de l'enregistrement au sein de son type. |
| `enabled` | booléen | Généralement `true`. |
| `note` | chaîne | Note en texte libre, généralement vide. |
| `owner_group` | UUID ou `null` | Groupe propriétaire de l'enregistrement (par exemple les administrateurs d'une organisation) ; `null` lorsqu'il appartient à l'utilisateur qui l'a créé. |
| `permissions` | objet | Indicateurs d'accès `group`, `all` et `other`, chacun `{ "read": boolean, "write": boolean }`. |
| `joined_collections` | `null` | Toujours `null` dans cette API. |

`created`, `updated`, `accessed`, `permissions` et `owner_group` sont gérés par le
serveur. Les valeurs que vous envoyez pour ces champs dans les requêtes de création ou de mise à jour sont ignorées.

## Interroger des listes

Les endpoints qui listent des enregistrements (`.../find`) et qui les comptent (`.../count`) prennent le même
objet de requête comme corps. L'exemple utilisé tout au long de cette section est
`POST /api/v3/conversation/find`.

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `filters` | tableau | non | Conditions que les enregistrements doivent remplir, voir [Filtres](#filtres). Toutes les conditions du tableau doivent être remplies. |
| `sort_key` | chaîne | non | Champ de tri, par exemple `"name"` ou `"updated.timestamp"` (notation pointée pour les champs imbriqués). Il doit s'agir d'un champ de l'enregistrement ; un champ inconnu est rejeté avec `422`. |
| `sort_order` | entier | non | `0` = croissant (par défaut lorsque `sort_key` est défini), `1` = décroissant. Ignoré sans `sort_key`. |
| `limit_from` | entier | non | Index du premier enregistrement à renvoyer, à partir de `0`. La pagination n'est appliquée que lorsque ce champ est défini. |
| `limit_to` | entier | non | Index **après** le dernier enregistrement à renvoyer. Il s'agit d'une position absolue, pas d'une taille de page : `limit_from: 20, limit_to: 40` renvoie les enregistrements 20 à 39. `null` ou `0` signifie « jusqu'à la fin ». |
| `fetch_dict` | booléen | non | N'a aucun effet sur les données renvoyées ; vous pouvez l'omettre. |
| `join` | tableau | non | Non pris en charge par l'API d'intégration ; ignoré. |

Sans `sort_key`, les enregistrements sont renvoyés du plus récent au plus ancien (par `created.timestamp`,
décroissant). Sans `limit_from`, tous les enregistrements correspondants sont renvoyés dans une seule réponse.

### Pagination

Pour lire la page *n* (en comptant à partir de 0) de taille *s*, envoyez `limit_from: n * s` et
`limit_to: (n + 1) * s`. Règles :

- `limit_from` doit valoir `0` ou plus et `limit_to` doit être supérieur à `limit_from`.
  Un `limit_from` négatif ou un `limit_to` inférieur à `limit_from` est rejeté avec
  `422`. Lorsque les deux sont égaux, aucune borne supérieure n'est appliquée et tous les enregistrements à partir de
  `limit_from` sont renvoyés.
- `limit_to` sans `limit_from` est ignoré : le résultat complet est renvoyé.
- Il n'existe pas de taille de page maximale côté serveur. Gardez des pages raisonnablement petites (jusqu'à quelques
  centaines d'enregistrements) ; les enregistrements volumineux, comme les conversations avec un long historique, produisent de grosses
  réponses.

```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
  }'
```

La réponse est un tableau d'enregistrements ; un tableau vide lorsque rien ne correspond :

```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" }
  }
]
```

Pour tout lire, demandez des pages jusqu'à ce que l'une d'elles soit plus courte que la taille de page.
Triez par `created.timestamp` croissant, afin que les enregistrements créés pendant la pagination soient
ajoutés à la fin au lieu de décaler les pages que vous n'avez pas encore lues :

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

### Comptage

Les endpoints `.../count` prennent le même corps et renvoient le nombre d'enregistrements qui correspondent à
`filters` sous forme d'entier simple. Ils ignorent `limit_from`, `limit_to` et les champs de
tri, si bien que vous pouvez envoyer le même corps que pour la page affichée et obtenir le total
pour la pagination.

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

Toutes les listes n'ont pas d'endpoint de comptage ; les pages des endpoints indiquent ceux qui existent.

## Filtres

`filters` est un tableau. Chaque élément est une condition ou un groupe de conditions, et un
enregistrement n'est renvoyé que s'il correspond à **tous** les éléments.

### Conditions

Une condition est un tableau `[field, operator, value]`, avec un quatrième élément facultatif
(voir [Conversion des UUID](#conversion-des-uuid)) :

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

`field` désigne un champ de l'enregistrement. Utilisez la notation pointée pour les champs imbriqués
(`created.timestamp`, `created.user_id`). Un chemin vers une liste d'objets correspond lorsque
n'importe quel élément de la liste correspond (`messages.role`). `id` est l'identifiant de l'enregistrement.

| Opérateur | Correspond lorsque le champ... | Valeur |
|---|---|---|
| `==` | est égal à la valeur | toute valeur autorisée |
| `!=` | n'est pas égal à la valeur | toute valeur autorisée |
| `<` | est inférieur à la valeur | nombre ou chaîne |
| `>` | est supérieur à la valeur | nombre ou chaîne |
| `<=` | est inférieur ou égal à la valeur | nombre ou chaîne |
| `>=` | est supérieur ou égal à la valeur | nombre ou chaîne |
| `regex` | contient une correspondance de l'expression régulière, sans tenir compte de la casse | chaîne |
| `in` | est égal à l'une des valeurs | tableau non vide, au plus 100 valeurs |

Remarques :

- `regex` est toujours insensible à la casse et non ancrée : `"invoice"` correspond à
  `"Invoices 2026"`. Utilisez `^` et `$` dans le motif pour l'ancrer, et échappez les caractères
  d'expression régulière (`.`, `*`, `+`, `?`, `(`, `)`, `[`, `]`, `\`) pour les faire correspondre
  littéralement.
- Les chaînes sont comparées alphabétiquement (par code de caractère), les nombres numériquement. Comparez
  les horodatages en tant que nombres.
- `["field", "==", null]` correspond aux enregistrements dont le champ vaut `null` ou est absent.

### Groupes

Un groupe est un objet avec exactement une clé, `AND` ou `OR`, dont la valeur est un tableau non vide
de conditions ou d'autres groupes. Les groupes peuvent être imbriqués.

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

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

La clé doit être écrite en majuscules. **Un objet avec toute autre clé (par exemple
`"or"`) est ignoré et correspond à tous les enregistrements**, si bien qu'une faute de frappe désactive silencieusement cette partie
du filtre. Un objet sans clé ou avec plus d'une clé, ainsi qu'un groupe dont la valeur n'est
pas un tableau non vide, sont rejetés avec `422`. Les groupes peuvent être imbriqués jusqu'à 32 niveaux.

### Exemples

```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"]]}
  ]
}
```

Le serveur vérifie et normalise chaque condition avant d'exécuter la requête. La plupart des
résultats vides surprenants proviennent de l'une des règles ci-dessous.

### Conversion des UUID

Une valeur de type chaîne qui est un UUID valide est convertie en UUID avant la comparaison. Les champs qui
contiennent des identifiants (`id`, `assistant_id`, `created.user_id`, ...) sont stockés sous forme d'UUID, c'est donc
ce que vous voulez pour eux. Un champ qui contient du **texte** ressemblant par hasard à un UUID
(y compris 32 chiffres hexadécimaux sans tirets) ne correspondra plus après la conversion.
Ajoutez `true` comme quatrième élément pour comparer la valeur en tant que texte brut :

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

Avec `in`, le quatrième élément s'applique à chaque valeur de la liste. Les valeurs à l'intérieur d'un
tableau utilisé avec `==` ou `!=` ne sont jamais converties.

### Échappement HTML

Dans les valeurs de type chaîne, `<`, `>`, `"` et `'` sont remplacés par `&lt;`, `&gt;`, `&quot;` et
`&#x27;` avant la comparaison. Un filtre portant sur un texte qui contient ces caractères ne
correspond donc pas aux enregistrements stockés avec les caractères bruts ; filtrez sur une partie du texte
qui ne les contient pas, par exemple avec `regex`.

### Limites des valeurs

| Valeur | Règle |
|---|---|
| chaîne | Au plus 1 000 caractères. Ne doit pas commencer par `$`. Les caractères nuls sont supprimés. |
| tableau | Au plus 100 éléments (y compris pour `in`). Chaque élément doit être une valeur autorisée. `in` exige au moins un élément. |
| types autorisés | chaîne, nombre, booléen, `null`, tableau. Un objet comme valeur est rejeté. |

### Noms de champs

Un nom de champ doit commencer par une lettre et ne contenir que des lettres, des chiffres, `_` et `.`
(au plus 128 caractères). Les autres noms sont rejetés avec `422`.

Les noms de champs dans les filtres ne sont pas vérifiés par rapport à l'enregistrement : un champ mal orthographié ne correspond à
rien avec `==` et à tout avec `!=`. (`sort_key`, en revanche, est vérifié.)

### Conditions mal formées

Une condition de moins de trois ou de plus de quatre éléments, ou un élément de
`filters` (ou d'un groupe) qui n'est ni un tableau ni un objet, est rejeté avec
`422`. `detail` indique le problème, par exemple
`A filter condition must be [field, operator, value] with an optional fourth element, got 2 elements`.

## Erreurs

Les erreurs utilisent les codes de statut HTTP standard et un corps JSON avec un champ `detail` :

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

`detail` est un message lisible en anglais. Ne l'analysez pas, sauf lorsqu'une page d'endpoint
documente un message précis.

Lorsque la requête elle-même ne correspond pas au schéma de l'endpoint (un champ manquant ou de
mauvais type, un UUID invalide, un en-tête manquant, pas de corps), le statut est `422` et
`detail` est une liste qui pointe vers chaque problème :

```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` est l'emplacement du problème : `body`, `query` ou `header`, suivi du
chemin du champ.

Une réponse diffère de cette forme : une défaillance inattendue du serveur peut renvoyer `500`
avec le corps en texte brut `Internal Server Error`.

Lisez d'abord le code de statut et considérez le corps comme une information facultative.

| Statut | Signification | Que faire |
|---|---|---|
| `401` | La clé API est vide, inconnue, supprimée, désactivée ou n'a pas une portée exigée par l'endpoint. `detail` vaut `API key not provided` ou `API key is invalid`. | Vérifiez la clé et ses [portées](authentication.md#portees). Ne réessayez pas sans modification. |
| `403` | La clé est valide, mais son utilisateur n'a pas le droit de faire cela : l'enregistrement appartient à quelqu'un d'autre, ou le compte n'a pas l'autorisation. `detail` vaut généralement `permission denied`. | Ne réessayez pas. |
| `404` | L'enregistrement n'existe pas (ou a été supprimé). | Ne réessayez pas. |
| `422` | La requête est invalide : erreurs de schéma (`detail` sous forme de liste, voir ci-dessus), un filtre ou une clé de tri rejetés, ou une règle métier. L'épuisement des crédits renvoie aussi `422`, avec `detail` `not enough user credit` ou `not enough organization credit`. | Corrigez la requête, ou rechargez des crédits. |
| `423` | La ressource est verrouillée par une autre opération (par exemple un enregistrement de la [base de données booster](booster-database.md)). | Réessayez après une courte pause. |
| `429` | Limite de débit dépassée, voir [Limites de débit](#limites-de-debit). | Attendez `Retry-After` secondes. |
| `500` | Erreur du serveur. | Réessayez plus tard avec un backoff ; signalez-la si elle persiste. |
| `502`, `503`, `504`, `529` | Un fournisseur d'IA ou un autre service en amont a échoué ou est surchargé. | Réessayez plus tard avec un backoff. |

L'API n'utilise pas `402` ; les erreurs de crédits sont des `422`, comme décrit ci-dessus. Un en-tête
`uni-api-key` absent (par opposition à une valeur vide ou erronée) est une erreur de schéma et
renvoie `422`, pas `401`.

Les réponses aux vérifications de clé API échouées (`401`) et aux requêtes soumises à la limite de débit (`429`) sont
volontairement retardées d'environ une seconde, pour ralentir les tentatives de deviner une clé. Tenez-en compte
dans vos délais d'expiration et ne considérez pas ce délai comme un problème du serveur.

Les erreurs qui surviennent après le début d'un [stream](streaming.md) sont signalées à l'intérieur du
stream, et non par un statut HTTP.

## Limites de débit

Les requêtes sont soumises à une limite de débit par adresse IP du client et par endpoint : chaque chemin d'endpoint
a ses propres compteurs, et toutes les requêtes d'une même adresse IP vers cet endpoint sont comptées
ensemble, quelle que soit la clé API utilisée. Les requêtes sont comptées même lorsqu'elles échouent, y compris
les requêtes rejetées pour une mauvaise clé API.

Chaque endpoint a trois limites vérifiées en même temps : par minute, par heure et par
jour. Ce sont des fenêtres glissantes : une requête compte dans une fenêtre pendant toute la durée de
cette fenêtre après avoir été effectuée, si bien que la capacité se libère progressivement plutôt qu'au début
de chaque minute ou heure. Chaque page d'endpoint indique son niveau :

| Niveau | Par minute | Par heure | Par jour |
|---|---|---|---|
| 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 |

Le niveau webhook s'applique uniquement au [webhook de workflow](workflows.md) public, qui
limite en plus chaque déclencheur séparément. Le niveau static est une protection contre l'afflux de requêtes pour
les endpoints interrogés souvent, comme [la version du serveur](account.md#obtenir-la-version-du-serveur).

Lorsqu'une limite est dépassée, l'API renvoie `429` avec un en-tête `Retry-After` : le nombre
de secondes entières avant que la fenêtre n'ait de la place pour une nouvelle requête.

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

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

Les réponses réussies ne portent aucun en-tête de limite de débit ; suivez donc vous-même votre
débit de requêtes. Pour rester dans les limites :

- Attendez au moins `Retry-After` secondes avant de réessayer ; ne réessayez jamais un `429` dans une boucle
  serrée.
- En cas d'échecs répétés (`429`, `5xx`), utilisez un backoff exponentiel avec une part d'aléa (jitter).
- Étalez les traitements par lots dans le temps au lieu de les envoyer en rafales, et mettez en cache les listes que vous lisez
  souvent au lieu de les récupérer à chaque utilisation.
- Plusieurs workers derrière une même adresse IP partagent les limites ; prévoyez un budget pour l'ensemble.

## Streaming

Le chat et certains endpoints de workflow peuvent transmettre leur sortie en streaming : la réponse est envoyée en
chunks pendant sa génération, au lieu d'un seul corps JSON à la fin. Le format des
chunks, le signal de fin de stream et la façon dont les erreurs sont signalées en cours de stream sont décrits
dans [Streaming](streaming.md).
