# Rate limits and quotas

> Request limits per key and per route, how questions are counted and which plans include how many, controls counted apart, and the 402 codes.

Three different mechanisms can stop a call, and they fail with different statuses, so it helps to tell them apart:

1. **Request rate**: too many requests in a minute. A `429`. Transient: wait and retry.
2. **Questions**: the workspace allowance, or the key's own monthly ceiling, is spent. A `402` (a `429` on the Anthropic dialect). Not transient: it needs a human decision.
3. **Plan features and volumes**: documents, seats, features your plan does not include. A `402` with a specific `code`.

Customers buy **questions**. A question is the unit on every price page, every invoice and every limit message.

## Request rate

Two layers apply.

**Per key.** A key created with `ratePerMinute` is limited to that many requests per calendar minute, counted in the database so the limit holds however many servers answer. Past it, the call is refused with `429` and `code: "rate_limited"`, and a `retry-after` header says how many seconds remain in the current minute (1 to 60). A key without `ratePerMinute` has no per-key limit.

**Per route.** Independently of the key, the routes below carry a ceiling per minute.

| Route | Limit per minute |
| --- | --- |
| `POST /v1/chat/completions`, `POST /v1/messages` | 60 |
| `POST /v1/knowledge/query` | 60 |
| `POST /v1/verify` | 60 |
| `POST /v1/knowledge-sources/upload` | 30 |

> **INFO: Treat these numbers as starting points**
> The route ceilings are set from current traffic and are revised as real usage comes in; they are not a contract. Design for a `429` by honouring `retry-after` rather than by hard-coding a rate. If you need a higher ceiling for a production integration, ask for it.

**Honour retry-after**

_TypeScript_

```typescript
const res = await fetch(url, init)
if (res.status === 429) {
  const wait = Number(res.headers.get('retry-after') ?? 1)
  await new Promise((r) => setTimeout(r, wait * 1000))
  // then resend once
}
```

_Python_

```python
res = requests.post(url, **kwargs)
if res.status_code == 429:
    time.sleep(int(res.headers.get("retry-after", "1")))
    # then resend once
```

## Questions

Every answer that calls a model consumes questions from the workspace allowance. **One question is one answer, up to 8 000 tokens of reading and writing combined.** An ordinary answer is exactly one question: measured on production usage, a typical document answer is about 1 500 tokens and a chat turn about 1 100. A very long exchange, such as a multi-round debate, counts as several.

The response `usage` object reports tokens (`prompt_tokens`, `completion_tokens`, `total_tokens`, or `input_tokens` and `output_tokens` on the Anthropic dialect). That is the real consumption by the model, for your own cost tracking. It is not the number of questions charged: questions are derived from it, weighted by the model that answered.

| Model class | Counts as | Examples |
| --- | --- | --- |
| Light | 1x | Local models, Mistral Small, GPT-4o mini, Llama, Qwen, Gemma, DeepSeek |
| Standard | 3x | Claude Haiku, Mistral Medium, GPT-4.1 mini |
| Advanced | 6x | Claude Sonnet, Mistral Large, GPT-4o, GPT-4.1 |
| Premium | 12x | Claude Opus |
| Frontier | 30x | The largest frontier models |

The multiplier applies to the tokens before they are converted to questions, so a larger model uses the allowance faster. With `pulse-auto`, Synapse picks a model that fits the task, which keeps simple calls in the cheapest class. With `pulse-sensitive` or `+sensitive` the call runs on a local model (Light class). The class that served a call is the one that was billed, and the certificate records the provider.

Background work, such as ingesting a document into Cortex or the night watch, consumes no questions. On a local installation (desktop or Box), nothing is metered per question at all.

## Plans

Plans are per user, per month, excluding VAT; annual billing offers two months. Allowances are pooled across the workspace: three users on Pro share 1 200 questions per month. The monthly window is anchored on the subscription start day.

|  | Essentiel | Pro | Cabinet |
| --- | --- | --- | --- |
| Price per user per month | 9.99 EUR | 19 EUR | 29 EUR |
| Questions per user per month | 150 | 400 | 1 000 |
| Documents per user | 300 | 2 000 | 10 000 |
| Light (answers from your documents) | Yes | Yes | Yes |
| Chat (reasoning with memory) | No | Yes | Yes |
| Connectors (Drive, mailbox, watched folders) | No | Yes | Yes |
| Proof journal (the Registre, certificate export) | No | No | Yes |
| Business modules | No | No | Yes |

| Item | Detail |
| --- | --- |
| Trial | 14 days, no card, 30 questions, 50 documents, with the features of Pro. When it ends, calls fail with `402 SUBSCRIPTION_INACTIVE`. |
| Question packs | One-time purchases on top of any paid plan: 100 questions for 5 EUR, 500 for 19 EUR, 2 000 for 59 EUR, excluding VAT. They do not expire while the subscription is active. The monthly allowance is consumed first, then packs. |
| Integrators | Client workspaces are metered per client, with a wholesale grid and a monthly statement. See [Integrators](https://dev.subsidia.protypa.fr/docs/integrators.md) and [Partner API](https://dev.subsidia.protypa.fr/docs/partner.md). |

## A per-key ceiling

A key can carry `monthlyQuestionLimit`, a ceiling lower than the workspace allowance, in questions per calendar month (UTC). It exists so that one runaway integration cannot spend the allowance of everyone else. Past it, every call that consumes questions fails with `402` and `code: "API_KEY_BUDGET_EXCEEDED"` until the next month or until the ceiling is raised. The usage behind the check is read from the usage ledger and cached for about 15 seconds, so a burst can overshoot by a few calls; use it as a guard rail, not as an exact meter. Set it when you create the key or later, see [Authentication](https://dev.subsidia.protypa.fr/docs/authentication.md).

## Controls: counted apart

A report from [Le Vérificateur](https://dev.subsidia.protypa.fr/docs/verify.md) calls **no model**, so counting it in questions, which are derived from model tokens, would price it as something it is not. It is counted in its own unit, a **control**: one control is one Vérificateur report, whether it came from the app or from `POST /v1/verify`. Consequences:

- A control consumes no questions and is not stopped by a key's `monthlyQuestionLimit`. Keys that only hold the `proof` scope are never blocked by it.
- `GET /v1/verify/usage` returns the controls used by your workspace this calendar month: `{ "month": "2026-10", "controls": 42 }`.
- Controls appear on an integrator's monthly statement, per client. For now no volume is included and no price is charged beyond the subscription: they are counted and shown, not billed.
- The Registre needs the proof journal feature (Cabinet plan). Without it, `/v1/registre` routes answer `402 PLAN_UPGRADE_REQUIRED`.

**Controls used this month**

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/verify/usage \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const usage = await fetch('https://api.subsidia.protypa.fr/v1/verify/usage', {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
}).then((r) => r.json())
console.log(usage.month, usage.controls)
```

_Python_

```python
import os, requests

usage = requests.get(
    "https://api.subsidia.protypa.fr/v1/verify/usage",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
).json()
print(usage["month"], usage["controls"])
```

## 402 codes

| Code | Fields | Cause | Resolution |
| --- | --- | --- | --- |
| `insufficient_quota` | (OpenAI envelope) | No questions left, on `/v1/chat/completions`. | Add a pack or upgrade. |
| `QUESTION_LIMIT` | `required`, `available` | Fewer questions left than the request needs, on native routes. | Add a pack or upgrade. |
| `API_KEY_BUDGET_EXCEEDED` | none | The key's own monthly ceiling is reached. | Raise the ceiling or wait for the next month. |
| `SUBSCRIPTION_INACTIVE` | none | Trial finished or subscription lapsed. | Choose a plan. |
| `PLAN_UPGRADE_REQUIRED` | `feature`, `requiredPlan` | The feature is not part of the current plan. | Move to `requiredPlan`. |
| `DOCUMENT_LIMIT` | `limit`, `used` | The document volume of the plan is reached. | Delete documents or upgrade. |
| `SEAT_LIMIT` | `seats` | The subscription covers fewer users than requested. | Add a seat. |

On `/v1/messages` a spent allowance is a `429` with `error.type: "rate_limit_error"` and no `retry-after`, because the Anthropic dialect has no payment status. Retrying does not help. See [Errors](https://dev.subsidia.protypa.fr/docs/errors.md).

**Which headers tell me how much I have left?**

Responses do not report the remaining allowance. A key-level 429 carries `retry-after`; do not build on any other rate-limit header. Read your remaining allowance in the console, and handle the first 402.

**Does a refused call cost a question?**

A call refused before the model runs (a 4xx) consumes nothing, including the 402 that tells you the allowance is spent.

**Why did one call cost more than one question?**

Either the exchange was long (more than 8 000 weighted tokens), or a larger model answered. With `pulse-auto` the cheapest adequate class is chosen; naming a specific large model, or sending `tools`, can move the call to a heavier class.
