Лимиты
| Эндпоинты | Лимит | Окно | Считается по |
|---|---|---|---|
Все /v2/* и /v1/* (live) | 20 / минуту | 60 сек | API-ключу |
Auth /auth/*, /login | 10 / минуту | 60 сек | IP |
POST /v2/qr и /v2/invoices | 1 / 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-Reset | Unix timestamp обнуления окна |
Retry-After | Только при 429: через сколько секунд можно ретраить |
HTTP/1.1 200 OK
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1731331860Ответ при превышении
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
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
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
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
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}"
doneBest 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.
Что дальше
- •Коды ошибок —
rate_limit_exceededи связанные - •Идемпотентность — обязательна при retry
- •Тестовый режим — sandbox без лимитов