# Лимиты и повторы

Лимиты считаются в весовых единицах: тяжелые запросы расходуют больше. Превышение дает 429, перегрузка или временная недоступность 503. Оба ответа говорят, когда повторить.

## Вес запроса

| Маршрут | Вес |
|---|---|
| `GET /v1/me` | 1 |
| `GET /v1/workspaces` | 1 |
| `GET /v1/campaigns` | 2 |
| `GET /v1/campaigns/{id}` | 1 |
| `GET /v1/leads` | 2 |
| `GET /v1/leads/{id}` | 1 |
| `GET /v1/leads/{id}/messages` | 3 |
| `GET /v1/events` | 2, с `include_text=true` 3 |

## Лимиты по тарифам

Лимиты действуют одновременно на ключ и на всю организацию, по всем ее ключам вместе.

| Окно | Pro | Premium | Enterprise |
|---|---|---|---|
| Ключ, в секунду | 5 | 10 | 10 |
| Ключ, в минуту | 300 | 600 | 600 |
| Организация, в секунду | 10 | 25 | 25 |
| Организация, в минуту | 600 | 1500 | 1500 |
| Одновременных запросов на ключ | 5 | 5 | 5 |
| Одновременных запросов на организацию | 10 | 10 | 10 |

Пример: на тарифе Pro один ключ может сделать 2 запроса списка лидов в секунду (вес 2, лимит 5) и до 150 таких запросов в минуту. Действующие лимиты ключа возвращает [`GET /v1/me`](https://docs.melioralab.io/reference/api-context/) в поле `limits`.

Кроме того, API ограничивает запросы с одного IP-адреса: в среднем 20 в секунду, кратковременно до 40. Если с одного адреса за 5 минут пришло 20 ответов 401, адрес блокируется на 15 минут: все его запросы получают 429 с `Retry-After`. Проверьте ключ, прежде чем повторять запросы с ошибкой 401.

Иногда, например во время работ на нашей стороне, лимиты временно снижаются. Тогда в `limits.mode` приходит `emergency`, а в полях лимитов меньшие значения. Код, который читает заголовки ответа, подстроится сам.

## Заголовки

Успешные ответы и ответы 429 по лимитам ключа и организации содержат состояние лимитов. Ответ 429 по лимиту IP-адреса приходит без этих заголовков, только с `Retry-After`.

```http
RateLimit-Policy: "key_second";q=5;w=1, "key_minute";q=300;w=60, "org_second";q=10;w=1, "org_minute";q=600;w=60
RateLimit: "key_second";r=3;t=1, "key_minute";r=291;t=60, "org_second";r=8;t=1, "org_minute";r=583;t=60
RateLimit-Limit: 5
RateLimit-Remaining: 3
RateLimit-Reset: 1
```

| Заголовок | Значение |
|---|---|
| `RateLimit-Policy` | Окна: имя, лимит `q` в весовых единицах и длина окна `w` в секундах. |
| `RateLimit` | По каждому окну остаток `r` и секунды до сброса `t`. |
| `RateLimit-Limit` | Лимит окна, которое ближе всего к исчерпанию. |
| `RateLimit-Remaining` | Остаток в этом окне. |
| `RateLimit-Reset` | Секунд до сброса этого окна. |

## Ответ 429

Если вес запроса больше остатка или превышено число одновременных запросов, приходит 429 `rate_limit_exceeded` с заголовком `Retry-After` в секундах. Запрос не выполнен. Подождите `Retry-After` секунд и добавьте случайный разброс, чтобы несколько процессов не повторили запрос одновременно.

## Ответ 503

503 означает, что запрос сейчас не выполнен. Код уточняет причину:

| Код | Причина | `Retry-After` |
|---|---|---|
| `server_busy` | Сервис перегружен | 1 секунда |
| `service_unavailable` | Данные временно не прочитаны | 5 секунд |
| `integration_initializing` | Новый ключ еще готовится | 5 секунд |

Повторяйте с растущей паузой: начните с `Retry-After`, затем удваивайте до 60 секунд. Если 503 приходит дольше нескольких минут, напишите в [поддержку](https://docs.melioralab.io/support/) и приложите `Request-Id`.

## Повтор безопасен

Все маршруты v1 только читают данные, поэтому любой запрос можно повторить без побочных эффектов. Ошибка не продвигает курсор: повтор с тем же курсором вернет ту же страницу.

```js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function meliora(path, params = {}, maxAttempts = 6) {
  const url = new URL(`https://api.melioralab.io/v1${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` },
        signal: AbortSignal.timeout(15_000),
      });
    } catch (networkError) {
      if (attempt >= maxAttempts) throw networkError;
      await sleep(Math.min(60, 2 ** attempt) * 1000);
      continue;
    }
    if (res.ok) return res.json();
    const problem = await res.json().catch(() => ({}));
    if ((res.status === 429 || res.status === 503) && attempt < maxAttempts) {
      const base = Number(res.headers.get('Retry-After')) || 5;
      const wait = Math.min(60, base * (res.status === 503 ? 2 ** (attempt - 1) : 1));
      await sleep((wait + Math.random() * Math.max(1, wait * 0.25)) * 1000);
      continue;
    }
    const error = new Error(`Meliora API ${res.status} ${problem.code ?? ''}`);
    Object.assign(error, { status: res.status, code: problem.code, requestId: res.headers.get('Request-Id') });
    throw error;
  }
}
```
```python
import os
import random
import time

import requests

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MELIORA_API_KEY']}"

class MelioraError(Exception):
    def __init__(self, status, code, request_id):
        super().__init__(f"Meliora API {status} {code}")
        self.status, self.code, self.request_id = status, code, request_id

def meliora(path, params=None, max_attempts=6):
    for attempt in range(1, max_attempts + 1):
        try:
            resp = session.get(f"https://api.melioralab.io/v1{path}", params=params, timeout=15)
        except requests.RequestException:
            if attempt == max_attempts:
                raise
            time.sleep(min(60, 2 ** attempt))
            continue
        if resp.ok:
            return resp.json()
        if resp.status_code in (429, 503) and attempt < max_attempts:
            base = int(resp.headers.get("Retry-After", "5"))
            wait = min(60, base * (2 ** (attempt - 1) if resp.status_code == 503 else 1))
            time.sleep(wait + random.uniform(0, max(1, wait * 0.25)))
            continue
        try:
            code = resp.json().get("code")
        except ValueError:
            code = None
        raise MelioraError(resp.status_code, code, resp.headers.get("Request-Id"))
    raise RuntimeError("unreachable")
```

## Как не упираться в лимиты

- Опрашивайте [ленту событий](https://docs.melioralab.io/events-polling/) раз в минуту вместо регулярного обхода всех лидов.
- Используйте `limit=100` для списков и до 200 для ленты: меньше запросов на тот же объем.
- Фильтруйте на нашей стороне: `campaign_id`, `status`, `username` дешевле, чем выгрузка всего списка.
- Не запускайте больше 5 параллельных запросов на ключ, лишние получат 429.
- Запрашивайте тексты только там, где они нужны: события с `include_text=true` и переписка весят 3.
