# Limites de débit

> Comment l'API borne les rafales par clé et par workspace, ce que disent les en-têtes de débit, ce qu'un 429 signifie et comment rejouer proprement.

L'API Transaction Intelligence protège son moteur contre les rafales par une limite opérationnelle de débit, publiée dans le contrat sous l'extension `x-noryo-rate-limit` du document [openapi.json](https://api.noryo.app/openapi.json), avec une version de politique (`policy_version` : `debit_ti_v1`).

## Ce que c'est, et ce que ce n'est pas

Le contrat la qualifie de protection opérationnelle contre les rafales (`kind` : `operational_burst_protection`) et dit explicitement ce qu'elle n'est pas :

| Ce qu'elle n'est pas, selon le contrat | Ce que cela signifie pour vous |
|---|---|
| `sandbox_lifetime_quota` | Elle ne compte pas vos requêtes sur la durée de vie du sandbox : le seau se recharge en continu. |
| `production_usage_quota` | Elle ne dit rien d'un volume autorisé en production, qui n'est pas ouverte. |
| `billing_entitlement` | Rien n'est facturé. |
| `commercial_promise` | Les nombres publiés sont des réglages versionnés, pas un engagement commercial. |

## Deux seaux à jetons

L'algorithme est un seau à jetons (`token_bucket`), appliqué à deux portées en même temps :

- **la clé** : chaque clé d'API a son propre seau ;
- **le workspace** : un second seau borne la somme de toutes les clés du workspace : cent clés ne font pas cent fois la limite.

Les valeurs en vigueur sont celles du contrat :

`debit_ti_v1` — operational_burst_protection · token_bucket

- par clé : 120 requêtes / minute (capacité 120, recharge 2/s)
- par workspace : 600 requêtes / minute (capacité 600, recharge 10/s)

« 120 requetes / minute » est traduit en seau a jetons : capacite 120 (la rafale toleree) rechargee a 2 jetons / seconde (le regime durable). Une clef au repos peut envoyer 120 requetes d'un coup, puis 2 par seconde ; sur la premiere minute apres un repos, jusqu'a 240 passent. Le workspace, lui, borne la somme de toutes ses clefs a 600 / minute (capacite 600, 10 / s) : cent clefs ne font pas cent fois la limite.

Ce n'est pas : `sandbox_lifetime_quota`, `production_usage_quota`, `billing_entitlement`, `commercial_promise`.

Chaque seau a une capacité (la rafale tolérée) et un rythme de recharge (le régime durable, en jetons par seconde). « N requêtes par minute » traduit ce couple : une clé au repos peut vider son seau d'un coup, puis continue au rythme de la recharge.

Le décompte est central et atomique, une requête à la fois, sans compteur local par serveur. L'environnement fait partie de l'identité du seau (`environment_scoped`) : les valeurs publiées sont celles du sandbox. La production n'est pas ouverte et ses réglages ne sont pas un engagement. Voir [Limites du sandbox](https://developer.noryo.app/fr-FR/transaction-intelligence/sandbox-limitations).

## Lire les en-têtes

Dès que la requête est authentifiée, la réponse porte l'état du débit, refus compris.

| En-tête | Présent sur | Signification |
|---|---|---|
| `noryo-ratelimit-limit` | succès, `422`, `404`, `409`, `429` | La capacité de rafale de votre clé. |
| `noryo-ratelimit-remaining` | succès, `422`, `404`, `409`, `429` | Ce que votre clé peut encore envoyer maintenant : le plus petit des deux seaux, celui de la clé et celui du workspace. |
| `Retry-After` | `429` seulement | Nombre entier de secondes avant que le seau qui a refusé couvre une nouvelle requête. |
| `noryo-request-id` | toute réponse rendue après authentification (toute erreur porte `request_id` dans son corps) | L'identifiant de la requête, à citer au support. Un `404` (chemin inconnu) ou un `405` (verbe non déclaré) vient de l'hébergeur, avant l'API, sans `request_id`. |

`noryo-ratelimit-remaining` peut valoir zéro alors que votre clé a encore des jetons : c'est alors le seau du workspace qui est vide.

## Quand la limite est atteinte : 429

Au-delà de la limite, l'API répond `429` avec un corps `application/problem+json` de type `rate_limited` et l'en-tête `Retry-After`. Rien n'a été calculé, rien n'a été enregistré, aucun événement n'a été émis. La requête peut être rejouée telle quelle après le délai.

```json
{
  "type": "https://noryo.app/problems/transactions/rate_limited",
  "title": "Trop de requetes.",
  "status": 429,
  "detail": "La limite de debit de cette clef est atteinte. Rejouez apres le delai de Retry-After.",
  "request_id": "req_01J9…"
}
```

Quand c'est le workspace qui a refusé, le `detail` le dit. `title` et `detail` sont des textes pour une personne, rédigés en français par l'API : appuyez votre logique sur `type` et `status`, jamais sur leur formulation. Voir le [catalogue des erreurs](https://developer.noryo.app/fr-FR/transaction-intelligence/errors/rate_limited) et la page [Erreurs](https://developer.noryo.app/fr-FR/transaction-intelligence/errors).

> **Note** Le limiteur ne fabrique jamais de 429 quand il est lui-même en panne : il répond `500` (`internal_error`) plutôt que de laisser passer sans limite.

## Ce qui consomme un jeton, ce qui n'en consomme pas

Le contrat fixe l'ordre de traitement : hôte, type de contenu, taille du corps, credential, puis authentification, préfixe, usage et débit dans une seule transaction, puis JSON, `Idempotency-Key`, schéma et métier. Le jeton est consommé dès que l'authentification réussit, avant toute lecture du corps : toute requête authentifiée en consomme un, quelle que soit sa réponse.

| Réponse | Jeton consommé | Pourquoi |
|---|---|---|
| `201` créé ; `200` lecture ou couverture enregistrée | oui | Requête authentifiée et traitée. |
| `200` rejeu idempotent | oui | Authentifiée et traitée, même si rien n'a été créé. |
| `422` schéma ; `404` introuvable ; `409` conflit d'idempotence | oui | La credential était valide ; le refus vient après le débit. |
| `400` JSON illisible ou `Idempotency-Key` hors alphabet | oui | Même raison. |
| `401` clé absente, mal formée, d'une autre famille, inconnue, révoquée, ou sous un préfixe divergent | non | Aucun seau touché, aucun usage marqué ; une credential inconnue n'apprend pas qu'un seau existe. |
| `403` workspace ou organisation inactif | non | Même règle que le 401. |
| `400` hôte non déclaré ; `413` ; `415` | non | Refusés avant toute lecture de la credential. |
| `GET /openapi.json` | non | Public, sans credential. |

Les trois opérations comptées sont `POST /transactions/v1/enrichments`, `GET /transactions/v1/enrichments/{enrichment_id}` et `POST /transactions/v1/coverage-windows`, un jeton chacune : une lecture coûte autant qu'un enrichissement.

## Rejouer proprement

1. **Respectez `Retry-After`.** Attendez au moins le délai indiqué : c'est le nombre de secondes avant que le seau qui a refusé couvre une nouvelle requête.
2. **Rejouez la même requête, avec la même `Idempotency-Key`.** Un 429 est décidé avant la lecture du corps : la requête rejouée est traitée comme une première fois. Si une tentative antérieure avait abouti sans que la réponse vous parvienne, la même clé rend un `200` de rejeu plutôt qu'un doublon. Voir [Enrichir une transaction](https://developer.noryo.app/fr-FR/transaction-intelligence/enrich-a-transaction).
3. **Bornez les tentatives et lissez l'envoi.** Quand les 429 se répètent, envoyez au rythme de la recharge plutôt que d'ajouter des tentatives : la capacité absorbe un pic occasionnel, elle n'est pas un débit tenable.
4. **Surveillez `noryo-ratelimit-remaining`** et ralentissez quand il approche de zéro.
5. **Coordonnez vos clés.** Répartir la charge sur plusieurs clés n'ajoute pas de capacité au-delà du seau du workspace.

Un exemple `fetch` qui honore `Retry-After` et conserve l'`Idempotency-Key` d'une tentative à l'autre :

```ts
async function enrichir(transaction: unknown, idempotencyKey: string, tentativesMax = 3) {
  for (let tentative = 1; ; tentative++) {
    const reponse = await fetch("https://api.noryo.app/transactions/v1/enrichments", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.NORYO_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(transaction),
    });

    if (reponse.status !== 429 || tentative >= tentativesMax) return reponse;

    // Retry-After est en secondes entières.
    const secondes = Number(reponse.headers.get("retry-after") ?? "1");
    await new Promise((resoudre) => setTimeout(resoudre, secondes * 1000));
  }
}

const reponse = await enrichir(
  {
    descriptor: "PRLV SEPA NETFLIX INTERNATIONAL",
    direction: "outgoing",
    occurred_at: "2026-09-05T03:10:00+02:00",
    amount: { value: "13.49", currency: "EUR" },
    payment: { rail: "sepa_direct_debit" },
  },
  crypto.randomUUID(),
);
console.log(reponse.status, reponse.headers.get("noryo-ratelimit-remaining"));
```

## Où observer les 429

Les 429 apparaissent sur Platform, page [Usage](https://platform.noryo.app/usage), section Transaction Intelligence, dans la catégorie des requêtes limitées, par clé et par workspace. Les 401 n'y figurent jamais : une clé révoquée ou inconnue ne compte pas. Voir [Usage et clés d'API](https://developer.noryo.app/fr-FR/transaction-intelligence/usage-and-api-keys).

---

Page canonique : https://developer.noryo.app/fr-FR/transaction-intelligence/rate-limits
