Лимиты запросов

Rate limits PayBot API — sliding-window лимиты по эндпоинтам и тарифам, заголовки X-RateLimit-*, код 429 и retry с exponential backoff.

PayBot применяет rate-limit, чтобы защитить Kaspi от перегрузки и обеспечить честное распределение между клиентами. Лимиты — sliding window, считаются на стороне Redis, headers возвращаются на каждом ответе.

Лимиты

ЭндпоинтыЛимитОкноСчитается по
Все /v2/* и /v1/* (live)20 / минуту60 секAPI-ключу
Auth /auth/*, /login10 / минуту60 секIP
POST /v2/qr и /v2/invoices1 / 2 минуты120 секIP
Public checkout /v2/checkout/*60 / минуту60 секIP
Webhook deliveries (исходящие)20 / секунду1 секцелевому URL

Enterprise-аккаунты освобождены от лимитов. Тестовые ключи kp_test_* не имеют rate-limits — удобно для нагрузочных тестов.

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

На каждом ответе PayBot возвращает:

HeaderОписание
X-RateLimit-LimitМаксимум запросов в окне
X-RateLimit-RemainingСколько осталось
X-RateLimit-ResetUnix timestamp обнуления окна
Retry-AfterТолько при 429: через сколько секунд можно ретраить
http
HTTP/1.1 200 OK
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1731331860

Ответ при превышении

http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Превышен лимит 20 запросов в минуту. Повторите через 12 секунд.",
    "request_id": "req_..."
  }
}

Стратегия retry

Используйте exponential backoff с jitter, ориентируясь на Retry-After.

Python

python
Скачать
import time, random, requests

def request_with_retry(method, url, **kwargs):
    for attempt in range(6):
        r = requests.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        wait = int(r.headers.get("Retry-After", 2 ** attempt))
        wait += random.uniform(0, 1)  # jitter
        time.sleep(wait)
    r.raise_for_status()

Node.js

javascript
Скачать
async function requestWithRetry(url, opts) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const r = await fetch(url, opts);
    if (r.status !== 429) return r;

    const retryAfter = Number(r.headers.get("retry-after") || 2 ** attempt);
    const jitter = Math.random();
    await new Promise(s => setTimeout(s, (retryAfter + jitter) * 1000));
  }
  throw new Error("rate limit retries exhausted");
}

PHP

php
Скачать
function requestWithRetry($url, $opts) {
    for ($attempt = 0; $attempt < 6; $attempt++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, $opts + [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => true,
        ]);
        $response = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $headers = substr($response, 0, $headerSize);

        if ($code !== 429) return $response;

        preg_match("/Retry-After: (\d+)/", $headers, $m);
        $wait = isset($m[1]) ? intval($m[1]) : pow(2, $attempt);
        sleep($wait + rand(0, 1));
    }
    throw new Exception("rate limit retries exhausted");
}

curl

bash
Скачать
for i in 0 1 2 3 4 5; do
  RESPONSE=$(curl -si -X POST https://api.paybot.kz/v2/payment-links \
    -H "X-API-Key: kp_live_xxx" \
    -d '{"amount": 5000}')
  CODE=$(echo "$RESPONSE" | head -1 | awk '{print $2}')
  if [ "$CODE" != "429" ]; then break; fi
  RETRY=$(echo "$RESPONSE" | grep -i "^Retry-After:" | awk '{print $2}' | tr -d '\r')
  sleep "${RETRY:-2}"
done

Best practices

  • Проверяйте X-RateLimit-Remaining заранее и притормаживайте, не дожидаясь 429
  • Очередь на стороне приложения — лучше задержать ваш ретрай, чем получить 429
  • Уникальный Idempotency-Key на retry — даже при 429 ваш ключ остаётся валидным, повтор не создаст дубль
  • Не открывайте сотни соединений — PayBot применяет и concurrent-limit, и общее окно
  • Бэкэнд для widget — если кнопка «Оплатить» на сайте, не вызывайте PayBot напрямую из фронта; делайте бэкенд-эндпоинт со своим rate-limit для пользователей

Burst и stable rate

Sliding window означает, что окно «скользит». Если вы сделали 20 запросов за 10 секунд, лимит исчерпан на ближайшие 50 секунд. Распределяйте нагрузку равномерно: 1 запрос каждые 3 секунды надёжнее, чем 20 разом.

Поднятие лимитов

Если штатно упираетесь в лимиты — это сигнал для тарифа Enterprise. Свяжитесь с поддержкой, опишите кейс — мы поднимем лимиты или предложим выделенный rate-limit pool.

Что дальше