Аутентификация
Каждый запрос к API несёт учётные данные в заголовке Authorization. Их три вида.
Секретный API-ключ (pd_…)
Полный доступ к одному приложению с ваших серверов. Создаётся в Приложения → Настройки → API-ключи; открытый текст показывается один раз при создании, далее хранится только хеш.
Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧРаботает и в REST API, и в GraphQL:
curl -X POST https://api.pushdata.ru/api/graphql \
-H "Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"query":"{ contacts(appId: \"APP_ID\") { id externalId } }"}'Безопасность: никогда не встраивайте секретный ключ в клиентский код и не публикуйте его в репозиториях. При утечке немедленно отзовите его (Приложения → Настройки → API-ключи → Отозвать) и создайте новый.
Разрешённые IP-адреса
Приложение может привязать секретные ключи к адресам своих серверов: Приложения → Настройки → API-ключи → Разрешённые IP-адреса (администраторы и владелец), до 20 адресов или диапазонов IPv4 / IPv6 (203.0.113.10, 198.51.100.0/24, 2001:db8::/32); allowedCidrs в updateApp. Каждый секретный ключ приложения — основной и созданные — с любого другого адреса отвечает 403 ip_not_allowed, а отказ попадает в журнал безопасности с приложением и адресом. Публичные ключи pk_ не ограничиваются: они живут в браузерах. Пустой список снимает ограничение. Учтите: адрес, с которого вы сами вызываете API (CI, ноутбук), тоже должен быть в списке.
Публичный API-ключ (pk_…)
Можно безопасно встраивать в браузеры и мобильные приложения. Ограничен публичной поверхностью независимо от схемы заголовка:
| Эндпоинт | Назначение |
|---|---|
POST /api/v1/public/devices, DELETE /api/v1/public/devices | Регистрация / деактивация вызывающего устройства |
POST /api/v1/events | Отчёт о delivered / opened / clicked |
GET /api/v1/public/inbox, …/unread_count, POST …/:id/read, POST …/read_all | In-app inbox вызывающего пользователя |
GET /api/v1/public/apps/:id/vapid | Публичный VAPID-ключ приложения для Web Push |
Authorization: Public pk_ВАШ_ПУБЛИЧНЫЙ_КЛЮЧЭти эндпоинты отвечают на CORS для любого origin. Публичный ключ на любом другом эндпоинте отклоняется с 403 forbidden.
Проверка идентичности
При включённой опции Приложения → Настройки → Проверка идентичности публичный запрос, заявляющий user_id, должен также нести X-Identity-Hash: <hex HMAC-SHA256(user_id, secret)>, вычисленный на вашем сервере с секретом проверки идентичности приложения. Без этого любой владелец публичного ключа мог бы зарегистрировать устройство (или прочитать inbox) под чужим id. Новое приложение создаётся с включённой проверкой: секрет показывается один раз при создании (рядом с API-ключом), в настройках его можно перевыпустить. Официальные клиенты шлют заголовок, как только ваш бэкенд выдаёт хеш: браузерный SDK через identityHash, iOS-пакет через identityHash, Android-клиент через registerDevice(token, userId, identityHash). Пока бэкенд его не выдаёт, регистрируйте устройства без user_id (анонимным устройствам хеш не нужен) или выключите проверку в настройках приложения — там при этом показывается предупреждение (user_id становится публичной информацией).
Песочница
Приложение с включённой Настройки → Песочница ведёт себя как обычно — отправки раскладываются по получателям, журнал доставок и счётчики заполняются, вебхуки уходят, сценарии выполняются, — но до провайдера ничего не доходит: каждая push-, e-mail-, SMS- и мессенджер-доставка записывается как отправленная с providerResponse.sandbox = true и не покидает PushData. In-app-сообщения доставляются по-настоящему (они живут в PushData). Две вещи делают песочницу полезной для staging-окружения:
- Тестовые получатели — ваши id пользователей (
user_idустройства,external_idконтакта) из настроек получают доставки по-настоящему: телефон QA или командный ящик видит то же, что увидел бы клиент. - Ключи, созданные в песочнице, начинаются с
pd_test_/pk_test_— сервер проверяет класс (pd_секретный,pk_публичный), инфикс нужен, чтобы его было видно в конфиге. Выданные раньше ключи продолжают работать.
Отправки в песочнице не расходуют лимит и не проверяются квотой; уведомление, отправленное в песочнице, несёт sandbox: true (REST GET /api/v1/notifications/{id}, GraphQL Notification.sandbox), каждая симулированная доставка — DeliveryLog.sandbox. Выключение песочницы не трогает уже отправленное. Отдельное приложение на окружение (dev / prod) остаётся самым простым разделением, когда контакты не должны пересекаться.
Роли в организации
Доступ к приложению даёт участие в его организации. owner управляет всем, включая биллинг, участников и удаление; admin дополнительно управляет API-ключами, учётными данными провайдеров и вебхуками; member делает повседневную работу (рассылки, контакты, шаблоны, каналы, сценарии); viewer всё видит и ничего не меняет: любая запись для viewer отклоняется с 403. API-ключи — не участники: секретный ключ действует с полными правами над своим приложением, публичный — только на публичной поверхности SDK.
Двухфакторная аутентификация
Второй фактор поверх беспарольного входа: после кода из письма (или входа через VK ID / Яндекс ID) у аккаунта с включённым фактором спрашивают шесть цифр из приложения-аутентификатора — Google Authenticator, Яндекс Ключ, 1Password, любое TOTP-приложение (RFC 6238, SHA-1, 30 секунд). Настройки → Безопасность → Двухфакторная аутентификация → Включить: дашборд показывает ключ (и ссылку otpauth:// для телефона), первый код из приложения подтверждает его, и один раз показываются восемь кодов восстановления — по одному входу без телефона. Код одноразовый; Новые коды восстановления заменяют набор; Выключить принимает код или код восстановления.
Для скрипта: verifyLoginCode вместо сессии отвечает twoFactorRequired: true и пятиминутным twoFactorToken; verifyTotp(input: { token, code }) возвращает сессию. Колбэк OAuth так же приводит на /login?totp=<token>.
Организация может требовать его: Команда → Требовать двухфакторную аутентификацию у всех участников (администраторы; у автора он уже должен быть включён). Участник без него по-прежнему входит, видит организации и настраивает фактор, но любая операция с приложениями и организацией отвечает 403 two_factor_required, пока фактор не включён. Включение и выключение фактора попадают в журнал действий каждой организации, где человек состоит.
Журнал действий
Каждое изменение на виду: Журнал (уровень организации, администраторы и владелец) показывает, кто что сделал и откуда — каждую мутацию дашборда (создан канал, изменена роль, заморожено приложение, отправка, стёрт контакт…) и каждый пишущий REST-запрос с секретным ключом приложений организации, с результатом (отклонённая попытка видна тоже, с кодом ошибки), адресом и клиентом. Аргументы хранятся с затёртыми секретами (токены, пароли, ключи, коды) и ограничены несколькими килобайтами. Фильтры по приложению, результату, источнику, периоду и тексту; Экспорт CSV (UTF-8 с BOM, до 1000 строк за вызов). То же для одного приложения через API — GET /api/v1/audit-log с секретным ключом (format=csv для файла) — и для скриптов auditEvents / auditEventsCsv в GraphQL. Записи хранятся 90 дней; выгружайте до истечения.
Сессия дашборда
Панель входит без пароля: одноразовый код на e-mail (requestLoginCode → verifyLoginCode, шесть цифр, 10 минут, пять попыток), VK ID или Яндекс ID (/api/auth/oauth/vk|yandex/start). Первый вход неизвестного адреса создаёт аккаунт и организацию на тарифе Free (пока регистрация открыта; в режиме «по приглашению» аккаунт создаёт только ссылка из раздела Команда, а страница входа об этом предупреждает). VK ID и Яндекс ID привязываются к аккаунту с тем же e-mail: Яндекс подтверждает адрес — привязка сразу; VK ID — после кода на этот адрес (страница входа запросит его сама). Привязанные входы видны в Настройки → Безопасность → Внешние входы, там же их можно отвязать — вход по коду остаётся всегда. Сервер отвечает JWT (7 дней) и ставит его HttpOnly-cookie pd_session (SameSite=Lax, Secure в production); сам дашборд токен в браузере не хранит и аутентифицируется только cookie. Скрипт или CLI могут вместо этого передавать полученный токен как Authorization: Bearer <token>. logout гасит cookie и отзывает сам токен сеанса (его копия перестаёт работать); signOutEverywhere — Настройки → Безопасность → «Выйти на всех устройствах» — завершает все сеансы аккаунта сразу. Сессии — для людей; интеграции должны использовать API-ключи.
Аккаунт принадлежит пользователю: exportMyAccountData возвращает всё, что платформа хранит о вошедшем пользователе (профиль без секретов, участие в организациях, внешние входы), а deleteAccount(code) удаляет его — код из письма, отправленного на адрес аккаунта (requestLoginCode для своего e-mail), почта здесь — второй фактор. Организации, которыми пользователь владеет, обрабатываются явно: есть другой владелец — пользователь просто выходит; пользователь единственный участник — организация удаляется с приложениями и данными; есть другие участники, но нет другого владельца — отказ 409 transfer_ownership_first; у организации есть платежи — отказ 409 organization_has_payments (финансовые записи сохраняются). Обе операции — в Настройки → Данные и Удалить аккаунт.
Лимиты запросов
Каждый ответ несёт X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset. Лимиты считаются на приложение (по классу ключа) и на IP для анонимных запросов; 429 rate_limited означает «подождите до момента сброса» (число секунд — в Retry-After). У публичного ключа свой бюджет на приложение и IP клиента, так что один посетитель не исчерпает бюджет серверной интеграции, а регистрация устройств по публичному ключу (POST /api/v1/public/devices) сидит в отдельной, более узкой корзине. 503 rate_limiter_unavailable с Retry-After означает, что хранилище лимитера было недоступно и запрос отклонён, а не пропущен: повторите через указанное число секунд.