1. API-ключ — только на сервере
Зачем. X-API-Key даёт право создавать счета от имени вашей организации, а при включённом флаге — управлять кассирами. Попав в браузер, публичный репозиторий или клиентскую часть Telegram-бота, ключ становится доступен любому.
Как проверить. Прогоните git grep по значению ключа во всей истории репозитория; убедитесь, что .env в .gitignore; откройте DevTools на своём сайте и проверьте, что в сетевых запросах фронтенда нет заголовка X-API-Key — запрос в ApiPay должен уходить с вашего бэкенда, а не из браузера покупателя.
2. Проверяйте подпись вебхука по сырому телу
Зачем. Без проверки подписи любой, кто узнал ваш webhook URL, пришлёт фейковое «счёт оплачен». Подпись — единственная гарантия, что уведомление действительно от ApiPay.
Как проверить. Считайте HMAC до того, как фреймворк распарсит и пересоберёт JSON: перестановка полей или пробелы изменят байты и сломают сверку. Формула и примеры кода — в статье «Настройка вебхуков ApiPay». Нажмите в кабинете «Проверить уведомления» на карточке ключа — придёт тестовое событие с боевой подписью; убедитесь, что ваш обработчик её принял.
3. Не путайте API-ключ и секрет подписи
Зачем. Это две разные строки с разными ролями. API-ключ (X-API-Key) — в заголовке ваших исходящих запросов к ApiPay. Секрет подписи вебхука (webhook secret) — для проверки входящих уведомлений (X-Webhook-Signature). Ключ наружу не показывают; секрет наружу тоже не показывают — но перепутать их местами означает, что ни аутентификация, ни проверка подписи не сработают. Подробный разбор — в статье «API-ключ и секрет подписи вебхука».
4. Защититесь от дублей идемпотентностью
Зачем. Сетевой сбой или ретрай на вашей стороне не должен превращаться во второй счёт покупателю. Передавайте external_order_id_idempotency (до 191 символа, уникален в пределах организации). Повтор с тем же ключом, пока прежний счёт жив, вернёт 409 duplicate_idempotency_key с invoice_id и статусом ранее созданного счёта.
Исключение — перевыставление: если прежний счёт с этим ключом уже в expired, cancelled или error, будет создан новый счёт, а не отдан 409. Один и тот же ключ идемпотентности может дать вам несколько счетов подряд — не считайте 409 единственным возможным ответом на повтор.
Как проверить. Отправьте один и тот же запрос дважды подряд, пока первый счёт в pending: первый — 201, второй — 409 с прежним invoice_id. Гонка двух параллельных запросов тоже безопасна: второй запрос получит 409 с тем же invoice_id.
5. При утечке ключа — «Сменить ключ» (секрет при этом НЕ меняется)
Зачем. Если ключ мог утечь — не ждите. В кабинете: Настройки → Подключение → карточка ключа → меню «Ещё» → «Сменить ключ». Старый ключ мгновенно перестаёт работать. Важно: смена ключа не трогает секрет подписи вебхука — если утёк и он, смените его отдельной кнопкой «Сгенерировать» у поля «Секретный ключ».
Как проверить. После смены ключа убедитесь, что старый возвращает 401, а новый работает; после смены секрета — что ваш обработчик пересчитывает подпись новым значением.
6. Ключ и секрет показываются только один раз
Зачем. Позже в карточке виден лишь хвост — ****key_hint. Восстановить полное значение нельзя, только перегенерировать.
Как проверить. Сразу при создании сохраните ключ и секрет в секрет-менеджер (Vault, GitHub Actions Secrets, переменные окружения хостинга) — не в заметки и не в код.
7. Ревизуйте доступы к кабинету
Зачем. В организацию можно пригласить до 10 сотрудников — лимит один на менеджеров и разработчиков. Уволенный сотрудник с доступом — это открытая дверь.
Как проверить. Раз в квартал открывайте в кабинете ApiPay пункт меню «Сотрудники» и убирайте тех, кто больше не должен иметь доступ.
8. Не путайте контуры песочницы и рабочего режима
Зачем. У клиента личного кабинета организация одна, и при переключении «песочница ↔ рабочий режим» API-ключ, Webhook URL и секрет остаются теми же. Значит ключ, которым вы тестировали, после переключения начнёт выставлять реальные счета реальным покупателям.
Как проверить. До переключения в рабочий режим уберите из конфигурации тестовые номера и сценарии, разведите контуры разными переменными окружения и проверьте, что боевой процесс не ходит по тестовым данным. Что происходит с тестовыми данными при переходе — «Песочница и рабочий режим».
9. Не логируйте полные ключи
Зачем. Логи утекают чаще, чем базы. Полный ключ в логе приложения = утечка.
Как проверить. В своих логах маскируйте секреты до последних 4 символов (****key_hint — ровно то, что показывает кабинет). Настройте фильтр логгера, который вырезает заголовки X-API-Key и X-Webhook-Signature.
10. Доверяйте подписи, а не IP
Зачем. Подлинность отправителя гарантирует подпись, а не IP-адрес. Ставьте HTTPS: http формально принимается, но тело и подпись поедут открытым каналом. Указывайте постоянный домен своего сервиса — не шортенер и не временную заглушку.
Как проверить. Не фильтруйте вебхуки по «белому списку IP» — фиксированный список адресов мы не публикуем. Единственная надёжная проверка — HMAC-подпись из пункта 2.
11. Ограничьте право ключа управлять кассирами
Зачем. У API-ключа есть флаг «управление кассирами». По умолчанию он выключен, и включить его может только владелец. Иначе утёкший ключ смог бы отключить кассира и остановить приём платежей.
Как проверить. Оставляйте флаг выключенным для всех ключей, которым это не нужно (обычные интеграции создания счетов в нём не нуждаются).
12. Ротируйте по расписанию и при смене команды
Зачем. Даже неутёкший ключ стоит менять периодически и обязательно — при уходе разработчика, у которого он мог остаться.
Как проверить. Заведите регламент: плановая ротация ключей и секретов, внеплановая — при любом кадровом или инфраструктурном изменении. Механика — пункт 5.
Вопросы и ответы
Смена API-ключа меняет секрет подписи вебхука?
Нет. Это две разные операции над записью ключа: «Сменить ключ» меняет только сам ключ, кнопка «Сгенерировать» у поля «Секретный ключ» — только секрет. Вебхук продолжает работать со старым секретом, пока вы не смените его отдельно.
Можно ли восстановить потерянный ключ или секрет?
Нет, они показываются один раз при создании. Потеряли — перегенерируйте (это сделает старое значение недействительным).