Лимиты и повторы
Лимиты считаются в весовых единицах: тяжелые запросы расходуют больше. Превышение дает 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 в поле limits.
Кроме того, API ограничивает запросы с одного IP-адреса: в среднем 20 в секунду, кратковременно до 40. Если с одного адреса за 5 минут пришло 20 ответов 401, адрес блокируется на 15 минут: все его запросы получают 429 с Retry-After. Проверьте ключ, прежде чем повторять запросы с ошибкой 401.
Иногда, например во время работ на нашей стороне, лимиты временно снижаются. Тогда в limits.mode приходит emergency, а в полях лимитов меньшие значения. Код, который читает заголовки ответа, подстроится сам.
#Заголовки
Успешные ответы и ответы 429 по лимитам ключа и организации содержат состояние лимитов. Ответ 429 по лимиту IP-адреса приходит без этих заголовков, только с Retry-After.
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 приходит дольше нескольких минут, напишите в поддержку и приложите Request-Id.
#Повтор безопасен
Все маршруты v1 только читают данные, поэтому любой запрос можно повторить без побочных эффектов. Ошибка не продвигает курсор: повтор с тем же курсором вернет ту же страницу.
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;
}
}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")#Как не упираться в лимиты
- Опрашивайте ленту событий раз в минуту вместо регулярного обхода всех лидов.
- Используйте
limit=100для списков и до 200 для ленты: меньше запросов на тот же объем. - Фильтруйте на нашей стороне:
campaign_id,status,usernameдешевле, чем выгрузка всего списка. - Не запускайте больше 5 параллельных запросов на ключ, лишние получат 429.
- Запрашивайте тексты только там, где они нужны: события с
include_text=trueи переписка весят 3.