--- title: "Авторизация" url: "https://docs.x-hub.online/start/auth/" description: "Ключ, подпись запроса и защита от повторов." relatedDocuments: - "https://docs.x-hub.online/start/quickstart/" - "https://docs.x-hub.online/start/sandbox/" --- # Аутентификация Все запросы к API (кроме health-check) требуют два заголовка: | Header | Значение | |---|---| | `X-Api-Key` | Публичный идентификатор мерчанта. Строка, начинающаяся с `xh_live_` (продакшн) или `xh_test_` (sandbox) | | `X-Api-Secret` | Секрет мерчанта (64 hex символа) | При невалидных ключах (в т.ч. без префикса `xh_live_`/`xh_test_`) — `401 Unauthorized`. ## Где хранить ключи - `api_key` — можно логировать, не секрет - `api_secret` — **только на твоём бэкенде** в env/secret manager, не в git - `webhook_secret` — **только на твоём бэкенде**, для проверки HMAC подписи webhook'ов !!! danger "Правила хранения" - Никогда не клади в git - Никогда не отдавай клиенту (браузер/мобилка) - Не пересылай в чатах - Логируй только `api_key`, не `api_secret` ## Idempotency-Key Обязательно для **всех POST** запросов, которые создают ресурсы. ``` Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 ``` Формат: UUID (любая версия, RFC 4122). Не-UUID строка → `400 VALIDATION_ERROR` (`"Idempotency-Key must be a valid UUID"`). ### Как работает - При первом вызове — создаётся новый ресурс - Повторный вызов с **тем же ключом** вернёт **исходный ресурс** (без дубликата) — в **любой момент**: уникальность ключа вечная в рамках пары (аккаунт, режим test/live). Статус-код при создании — `201`, при idempotent-повторе — `200` - Срока годности у ключа нет: повтор с тем же ключом хоть через месяц вернёт исходный ресурс, а не создаст новый - Тело запроса при повторе **не сверяется** — возвращается ответ первого вызова. Генерируй новый `Idempotency-Key` для каждой логически новой операции - Идемпотентность **изолирована между режимами**: тот же ключ в тестовом (`xh_test_`) и боевом (`xh_live_`) режимах — это два независимых платежа ### Зачем Предотвращает двойное списание при сетевых сбоях. Если твой POST упал по таймауту — ретрай с тем же ключом безопасен. ### Плохо и хорошо ```javascript // ❌ НЕПРАВИЛЬНО: генерируем UUID на каждый retry async function createPayment(amount) { return fetch('...', { headers: { 'Idempotency-Key': crypto.randomUUID() }, // каждый retry = новый UUID → дубли }); } // ❌ НЕПРАВИЛЬНО: не-UUID строка // 'Idempotency-Key': `order-${orderId}` → 400 VALIDATION_ERROR // ✅ ПРАВИЛЬНО: UUID генерируется один раз на заказ, // сохраняется рядом с заказом в своей БД и переиспользуется при ретраях async function createPayment(orderId, amount) { let idempotencyKey = await db.getIdempotencyKey(orderId); if (!idempotencyKey) { idempotencyKey = crypto.randomUUID(); await db.saveIdempotencyKey(orderId, idempotencyKey); } return fetch('...', { headers: { 'Idempotency-Key': idempotencyKey }, // тот же UUID при ретраях того же заказа }); } ``` ## Rate Limiting Лимитов **четыре**, и они разбиты на две независимые корзины: «денежные» ручки (создание приёма денег) и всё остальное в `/api/v1`. Внутри каждой корзины отдельно считаются лимит на ключ и лимит на IP. **Денежными считаются ровно эти вызовы:** - `POST /payments` - `POST /exchanges` - `POST /subscriptions` - `POST /subscriptions/{id}/charges` Список закрытый — денежная ровно эта четвёрка. Всё остальное идёт в общую корзину, включая вложенные пути: `POST /refunds`, `POST /refunds/{id}/cancel`, `POST /subscriptions/{id}/cancel`, `POST /kyc/register`. | Корзина | Лимит | На что считается | Окно | Порог | |---|---|---|---|---| | Денежные ручки | Per-key | значение заголовка `X-Api-Key` | 60 сек | **120 запросов** | | Денежные ручки | Per-IP | IP-адрес, с которого пришёл запрос | 60 сек | **120 запросов** | | Всё остальное в `/api/v1` | Per-key | значение заголовка `X-Api-Key` | 60 сек | **60 запросов** | | Всё остальное в `/api/v1` | Per-IP | IP-адрес, с которого пришёл запрос | 60 сек | **300 запросов** | Корзины не сообщаются друг с другом. Шторм опроса статусов выедает только свои 60/300 и **не может** заблокировать создание платежа — ради этого разделение и сделано. !!! warning "Для одного IP это ужесточение" Раньше денежные запросы делили общий IP-бюджет в 300 req/min. Теперь у них собственные **120** req/min на адрес. Если несколько твоих сервисов или несколько мерчант-аккаунтов создают платежи через один NAT/egress-адрес, вы упрётесь в лимит **раньше**, чем до этого изменения. Планируй нагрузку по созданию платежей отдельно от нагрузки по чтению. При превышении любого — `429 Too Many Requests` с кодом `RATE_LIMIT_EXCEEDED`. Различить, какой именно лимит сработал, можно по `message` и по значению `X-RateLimit-Limit`: ```json {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests. Limit: 60 per minute."}} ``` ```json {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests for API. Limit: 300 per minute per IP."}} ``` ```json {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many payment requests. Limit: 120 per minute. Retry with the same Idempotency-Key."}} ``` ```json {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many payment requests from this IP. Limit: 120 per minute. Retry with the same Idempotency-Key."}} ``` Повтор отбитого денежного запроса безопасен: с тем же `Idempotency-Key` второй платёж не создастся. !!! warning "Все лимиты работают ДО авторизации" Счётчики увеличиваются на **каждом** запросе, даже если ключ неверный, отозван или относится к неактивному режиму. Запрос, который в итоге получит `401`, всё равно съедает квоту. Практические следствия: - Per-key лимит считается по **строке ключа**, а не по аккаунту. Тестовый (`xh_test_`) и боевой (`xh_live_`) ключи — независимые бюджеты. После ротации ключа новый ключ стартует с чистым счётчиком. - Per-IP лимит — общий на весь исходящий IP. Если несколько твоих сервисов (или несколько мерчант-аккаунтов) ходят к нам через один NAT/egress-адрес, они делят одни и те же 300 req/min на чтение и одни и те же 120 req/min на создание платежей. Ретрай-штормы одного сервиса упрутся в лимит у соседнего. Заголовки ответа (отдаём оба семейства — legacy и draft-6 standard) приходят на **каждом** ответе, не только на `429`: ``` X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1776166800 RateLimit-Policy: 60;w=60 RateLimit-Limit: 60 RateLimit-Remaining: 42 RateLimit-Reset: 38 ``` - `X-RateLimit-Reset` — Unix timestamp (секунды с эпохи), когда окно сбрасывается - `RateLimit-Reset` (draft-6) — секунды **до** сброса окна - `Retry-After` (секунды) — добавляется **только** к ответу `429` Заголовки описывают тот лимит, до которого запрос дошёл последним. Порядок проверки: сначала IP-лимит своей корзины, затем key-лимит своей корзины. Для чтения в штатной ситуации это `Limit: 60`, для создания платежа — `Limit: 120`. Если запрос отбит IP-лимитом, до key-счётчика он не доходит — тогда в заголовках будет `Limit: 300` (чтение) или `Limit: 120` (создание платежа). Если получил `429` — подожди `Retry-After` секунд (или до `X-RateLimit-Reset`) и ретрай. ---