# Erreurs et limites

## Format

Toutes les erreurs ont la même forme :

```json
{
  "error": {
    "code": "origin_not_allowed",
    "message": "Cette page n'est pas autorisée pour cette clé.",
    "request_id": "req_7c1e…"
  }
}
```

- Basez votre code sur `code`, qui est stable ; `message` est une explication lisible qui peut évoluer.
- Le `request_id` est aussi dans l'en-tête `X-Request-Id`. Donnez-le au support (**contact@twimi.ai**) : il permet de retrouver la requête dans nos journaux.

## Codes

| HTTP | `code` | Sens | Que faire |
| --- | --- | --- | --- |
| 400 | `invalid_request` | requête mal formée ; le message nomme les champs | corriger la requête |
| 401 | `invalid_key` | clé absente, inconnue, révoquée, ou d'un autre environnement ; avec un jeton : la clé qui a ouvert la conversation a été révoquée | vérifier la clé et l'environnement |
| 401 | `token_expired` | jeton de conversation périmé | rouvrir avec `resume` |
| 401 | `invalid_token` | jeton invalide | ouvrir une nouvelle conversation |
| 403 | `origin_not_allowed` | page non déclarée pour cette clé publique | faire déclarer la page |
| 403 | `formula_not_allowed` | fonction non incluse dans la formule | — |
| 403 | `deployment_suspended` | déploiement suspendu | contacter Twimi |
| 403 | `secret_key_required` | route réservée à la clé secrète (`/v1/usage`) | appeler depuis le serveur |
| 403 | `invalid_link` | lien de fichier modifié ou expiré | relire la fiche de l'avatar |
| 404 | `not_found` | adresse inconnue | — |
| 404 | `avatar_not_found` | avatar hors du déploiement | vérifier `avatar_id` |
| 404 | `conversation_not_found` | conversation inconnue ou terminée | ouvrir une nouvelle conversation |
| 409 | `voice_limit_reached` | voix indisponible : forfait, plafond, fusible ou réponse de plus de 1 500 caractères | continuer par écrit (voir `X-Twimi-Voice-Reason`) |
| 410 | `conversation_expired` | conversation de plus de 10 minutes | ouvrir une nouvelle conversation |
| 413 | `audio_too_large` | son de plus de 5 Mo (`/transcriptions`) | raccourcir l'enregistrement |
| 429 | `rate_limited` | trop de requêtes | attendre la durée de `Retry-After` |
| 500 | `internal_error` | erreur de notre côté ; aucun détail interne n'est renvoyé | réessayer, sinon support |
| 502 | `voice_unavailable`, `transcription_unavailable`, `brain_unavailable`, `knowledge_unavailable` | service momentanément indisponible | réessayer un peu plus tard |

Sur `/messages`, la réponse étant en flux, un échec arrive comme un événement :

```text
data: {"type": "error", "code": "brain_unavailable", "message": "…"}
```

## Limites de débit

| Quoi | Limite |
| --- | --- |
| Ouvertures de conversation | 60 par minute et par déploiement |
| Questions (`/messages`) | 20 par minute et par conversation |
| Morceaux de voix (`/speech`) | 60 par minute et par conversation |
| Transcriptions | 20 par minute et par conversation |

Au-delà : `429 rate_limited`, avec l'en-tête `Retry-After` (en secondes).

```js
async function appel(url, options, essais = 3) {
  const res = await fetch(url, options);
  if (res.status === 429 && essais > 0) {
    const attente = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, attente * 1000));
    return appel(url, options, essais - 1);
  }
  return res;
}
```

## Autres limites

| Quoi | Limite |
| --- | --- |
| Question (`message`) | 1 000 caractères |
| Morceau de voix (`text`) | 2 000 caractères |
| Voix par réponse (un tour) | 1 500 caractères, tous morceaux compris |
| Son d'une question orale | 5 Mo, 30 secondes |
| Durée d'une conversation | 10 minutes |
| Validité d'un jeton | 30 minutes |
