Rate Limiting
Чтобы защитить платформу от резких бросков нагрузки и фрода, Finance OS API применяет лимиты на каждый ключ и каждый IP. Лимиты - мягкие: они не блокируют аккаунт, а только просят клиент сбавить темп и попробовать снова через Retry-After секунд.
Лимиты по умолчанию
Группы лимитов
authenticated
60 req/min
optional
guest
10 req/min
optional
webhook
без лимита
optional
мутации
30-120 req/min
optional
| Name | Type | Required | Description |
|---|---|---|---|
authenticated
|
60 req/min
|
optional | Запросы с валидным API-ключом. Считается на ключ, не на IP. |
guest
|
10 req/min
|
optional | Публичные endpoints без токена. Считается на IP. |
webhook
|
без лимита
|
optional | Входящие webhooks от платёжных и compliance-провайдеров — мы их доверяем по подписи, не по rate-limit. |
мутации
|
30-120 req/min
|
optional | Денежные и регистровые POST Merchant API - у каждой группы своя корзина, чтобы частое чтение не выедало лимит платежа. Точный порог - в разделе конкретного эндпоинта. |
Заголовки ответа
Каждый ответ (и успешный, и ошибочный) содержит три заголовка:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716826421
X-RateLimit-Limit
integer
optional
X-RateLimit-Remaining
integer
optional
X-RateLimit-Reset
integer (Unix timestamp)
optional
| Name | Type | Required | Description |
|---|---|---|---|
X-RateLimit-Limit
|
integer
|
optional | Максимум запросов в минуту в текущей группе. |
X-RateLimit-Remaining
|
integer
|
optional | Сколько осталось до окончания текущего окна (1 минута). |
X-RateLimit-Reset
|
integer (Unix timestamp)
|
optional | Когда окно обнулится. Можно использовать для backoff. |
Когда лимит превышен
HTTP 429 Too Many Requests, тело:
{
"message": "Too Many Attempts.",
"retry_after": 38
}
И заголовок Retry-After: 38 (секунды до возобновления). Клиент должен не повторять запрос немедленно — это эскалирует penalty.
Стратегия backoff
Рекомендация:
- Поймали 429 → читаем
Retry-After - Ждём
Retry-After + jitter(0..5s)— jitter избегает «громыхающего стада» - Повторяем запрос
- Если снова 429 — удваиваем delay (exponential backoff)
- После 3 подряд 429 — фейлим операцию и алертим оператора
Параллельные запросы
Лимиты считаются по факту обработки — не по количеству открытых соединений. Поэтому несколько параллельных запросов от одного клиента съедают лимит так же, как последовательные. Если нужно много читать (например, синхронизация истории) — используйте пагинацию с per_page=100, а не 100 параллельных запросов с per_page=1.
Примеры кода
# Проверить текущие лимиты — любой запрос вернёт их в заголовках
curl -I -X GET https://fin-os.io/api/v1/merchant/balance \
-H "Authorization: Bearer ${TOKEN}"
# Пример ответа:
# HTTP/2 200
# x-ratelimit-limit: 60
# x-ratelimit-remaining: 47
# x-ratelimit-reset: 1716826421
// Backoff helper
async function callWithBackoff(url, init, attempt = 1) {
const res = await fetch(url, init);
if (res.status === 429 && attempt <= 3) {
const wait = (parseInt(res.headers.get('Retry-After') || '1') + Math.random() * 5) * 1000;
console.warn(`429 — retry in ${Math.round(wait)}ms (attempt ${attempt})`);
await new Promise(r => setTimeout(r, wait));
return callWithBackoff(url, init, attempt + 1);
}
return res;
}
const balance = await callWithBackoff('https://fin-os.io/api/v1/merchant/balance', {
headers: { 'Authorization': `Bearer ${TOKEN}` },
});
// Laravel HTTP retry с обработкой 429
$response = Http::withToken($token)
->retry(3, function ($exception, $request) {
// Если 429, ждём Retry-After + jitter
$retryAfter = (int) ($exception->response->header('Retry-After') ?? 1);
return ($retryAfter + rand(0, 5)) * 1000; // ms
}, throw: false)
->get('https://fin-os.io/api/v1/merchant/balance');
// Проверим оставшийся лимит
$remaining = (int) $response->header('X-RateLimit-Remaining');
if ($remaining < 5) {
logger()->warning('Finance OS API rate near exhaustion', ['remaining' => $remaining]);
}
import requests, time, random
def call_with_backoff(url, headers, max_attempts=3):
for attempt in range(1, max_attempts + 1):
r = requests.get(url, headers=headers)
if r.status_code != 429:
return r
wait = int(r.headers.get('Retry-After', 1)) + random.uniform(0, 5)
print(f'429 — sleeping {wait:.1f}s (attempt {attempt})')
time.sleep(wait)
return r
balance = call_with_backoff(
'https://fin-os.io/api/v1/merchant/balance',
headers={'Authorization': f'Bearer {TOKEN}'},
)
print('Remaining:', balance.headers['X-RateLimit-Remaining'])