Finance OS / API

Rate Limiting

Чтобы защитить платформу от резких бросков нагрузки и фрода, Finance OS API применяет лимиты на каждый ключ и каждый IP. Лимиты - мягкие: они не блокируют аккаунт, а только просят клиент сбавить темп и попробовать снова через Retry-After секунд.

Лимиты по умолчанию

Группы лимитов

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 - у каждой группы своя корзина, чтобы частое чтение не выедало лимит платежа. Точный порог - в разделе конкретного эндпоинта.
Нужно больше?
Если ваш use-case требует более высокого RPS - напишите на admin@fin-os.io с указанием домена и ожидаемого паттерна нагрузки. Лимиты на партнёрских ключах настраиваются индивидуально.

Заголовки ответа

Каждый ответ (и успешный, и ошибочный) содержит три заголовка:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716826421
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

Рекомендация:

  1. Поймали 429 → читаем Retry-After
  2. Ждём Retry-After + jitter(0..5s) — jitter избегает «громыхающего стада»
  3. Повторяем запрос
  4. Если снова 429 — удваиваем delay (exponential backoff)
  5. После 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'])