Уведомления
Уведомления — это сообщения, которые доставляются на устройства через push-провайдеры (FCM, APNs, Web Push) или контактам через канал (email, SMS, MAX, in-app, Telegram). Здесь описан REST API (/api/v1) в том виде, в котором его реализует сервер; GraphQL API, которым пользуется дашборд, предоставляет те же операции.
Аутентификация
Есть два вида API-ключей (Приложения → Настройки → API-ключи):
| Ключ | Заголовок | Где может храниться | Операции |
|---|---|---|---|
Секретный pd_… | Authorization: ApiKey pd_… | Только ваши серверы | Всё, что описано ниже |
Публичный pk_… | Authorization: Public pk_… | Браузеры, мобильные приложения | Регистрация устройств, события вовлечённости, inbox, VAPID-ключ (/api/v1/public/*) |
Публичный ключ на секретном эндпоинте получает 403 forbidden.
Отправка уведомления
curl -X POST https://api.pushdata.ru/api/v1/notifications \
-H "Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123-placed" \
-d '{
"title": "Новый заказ",
"body": "Ваш заказ №123 оформлен.",
"target": { "user_ids": ["user-123"] },
"data": { "orderId": "123", "action": "open_order" },
"click_action": "https://shop.example/orders/123"
}'Ответ 201:
{ "id": "0192…", "status": "queued" }status равен queued для немедленной отправки (веерная рассылка выполняется асинхронно) и scheduled, если задан scheduled_at.
Тело запроса
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
title | string | ✓ | Заголовок (≤ 1024 символов) |
body | string | ✓ | Текст (≤ 4096 символов) |
target | object | ✓ | Ровно один из ключей ниже (для канальной отправки — см. следующий раздел) |
target.all | true | Все активные устройства приложения | |
target.device_ids | string | Конкретные id устройств (≤ 10 000) | |
target.user_ids | string | Устройства, зарегистрированные с этими user_id | |
target.tags | string | Устройства, у которых в tags есть любой из этих тегов | |
data | object | Произвольные ключ-значение, ≤ 4 КБ в сериализованном виде. Доставляется как есть; сервер добавляет pushdata_notification_id / pushdata_device_id для трекинга | |
click_action | string | URL / deep link, открываемый по нажатию (deep_link принимается как синоним) | |
icon | string | URL иконки (Android, Web Push) | |
image / image_url | string | URL большого изображения (FCM, Web Push; в APNs передаётся как pushdata_image_url для service extension) | |
badge | number | Счётчик на иконке (iOS) | |
sound | string | Имя звука (по умолчанию "default") | |
scheduled_at | ISO 8601 | Отложенная отправка; до этого момента статус scheduled. С local_time — время без пояса YYYY-MM-DDTHH:MM | |
local_time | boolean | «В это время по часам каждого получателя» — см. Ограничения отправки | |
recurrence | object | { every: day | week | month, interval?, until? } — та же отправка снова начиная со scheduled_at, см. Повторяющиеся отправки | |
transactional | boolean | Служебная отправка: без тихих часов и лимита в сутки (код, статус заказа). category с обязательной темой освобождена сама по себе | |
send_at_best_time | boolean | Каждый получатель ждёт свой лучший час — см. Лучшее время отправки. Не сочетается с local_time и transactional | |
expires_at | ISO 8601 | TTL push-уведомления: после этого момента не доставляется (APNs expiration, FCM ttl, Web Push TTL) | |
variants | object | A/B-тест: 2–4 { key?, title, body, weight? } — см. A/B-тесты и цели. title / body тогда можно не передавать | |
goal | object | { event, window_hours? } — цель конверсии, см. ниже | |
ab_test | object | { share, window_hours, metric? } — досылка победителя: тест уходит доле share аудитории, победитель — остальным через window_hours; нужны variants, см. ниже | |
apns_category, actions | Сохраняются внутри data |
template, vars, recurrence* и email_to этим сервером не поддерживаются и отклоняются с 422.
Отправка через канал
Тот же вызов с channel_type уходит не на устройства, а контактам через канал приложения: email, sms, telegram, max, in_app — адрес берётся из контакта (email, phone, telegramChatId, maxUserId, сам контакт для inbox). target для канальной отправки описывает контакты: all (все контакты с адресом для этого канала), user_ids (контакты с этими externalId), contact_ids (PushData-id) или segment_id (сохранённый сегмент); device_ids и tags — понятия push, для канала они отклоняются с 422. Telegram — единственный канал, где target можно не передавать: тогда сообщение уходит в чат, указанный в канале (группу или канал), а не контактам; без ID чата в канале — 400.
curl -X POST https://api.pushdata.ru/api/v1/notifications \
-H "Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "email",
"category": "orders",
"title": "Заказ №123 оформлен",
"body": "Собираем заказ, курьер позвонит перед доставкой.",
"target": { "user_ids": ["user-123"] }
}'| Поле | Тип | Описание |
|---|---|---|
channel_type | push | email | sms | telegram | max | in_app | Без поля — push. Регистр не важен |
channel_id | string | Конкретный канал приложения; без него берётся самый ранний активный канал этого типа. Неизвестный или чужой — 404, выключенный — 400 |
category | string | Категория подписки (≤ 100 символов): контакты, отключившие её для этого канала в настройках, пропускаются |
target.contact_ids | string | PushData-id контактов (≤ 10 000); только для канальных отправок |
target.segment_id | string | Сохранённый сегмент: те, кто подходит под его правила в момент отправки — для push устройства этих контактов, для канальной отправки сами контакты. Неизвестный или чужой — 404 |
title становится темой письма (или первой строкой SMS / сообщения), body — текстом; data доставляется в in-app-сообщении и в вебхуках. У email, SMS, Telegram и MAX нет подтверждения прочтения, поэтому total_delivered для них остаётся 0 (см. ниже).
AI-помощник текста
Когда на установке задан ANTHROPIC_API_KEY, на странице отправки под сообщением появляется строка Помощник: Заголовки (три предложения), Варианты A/B (две альтернативы, которые одним нажатием становятся вариантами теста), Короче, Официальнее, перевод на английский / русский и своя просьба («добавь срочности», «убери жаргон»). Предложение применяется к заголовку / тексту одним нажатием; до этого ничего не меняется. Модели уходят только текст сообщения и просьба — без контактов, адресов и метаданных, — а переменные {{ … }} сохраняются. Организации положено AI_ASSIST_PER_DAY запросов в сутки (200 по умолчанию; сверх — 429 ai_limit); каждый вызов попадает в журнал действий. GraphQL: assistCopy(input: { appId, task, title, body, channelType, locale, tone, instruction }); installationInfo.aiAssistant говорит, включён ли он. Без ключа строки нет, а мутация отвечает 400 ai_not_configured.
Кнопки в мессенджерах
actions — то же поле, что у push, — становятся кнопками под сообщением в Telegram, MAX и VK: inline-клавиатура в Telegram, вложение inline_keyboard в MAX, клавиатура open_link в VK. Кнопка — { "title", "url" } (читаются и label / text, и link), до 8, по одной в ряд; текст обрезается до 64 символов (40 в VK). Каждая ссылка идёт через трекер кликов: нажатие считается clicked в журнале доставок и в вебхуках, а затем открывает адрес. В блок данных текста сообщения кнопки не попадают.
curl -X POST https://api.pushdata.ru/api/v1/notifications \
-H "Authorization: ApiKey pd_YOUR_SECRET_KEY" -H "Content-Type: application/json" \
-d '{ "channel_type": "telegram", "title": "Заказ №123 уже в пути", "body": "Курьер приедет с 18:00 до 20:00.",
"target": { "user_ids": ["user-123"] },
"actions": [{ "title": "Отследить заказ", "url": "https://shop.example.com/orders/123" }, { "title": "Перенести", "url": "https://shop.example.com/orders/123/slot" }] }'Ограничения отправки: тихие часы, лимит в сутки, местное время
Три настройки приложения (Настройки → Ограничения отправки или updateApp в GraphQL: dailyCap, quietHoursStart / quietHoursEnd в минутах от полуночи, timezone) действуют на каждую неслужебную отправку — REST, GraphQL, дашборд и шаги SEND сценариев одинаково:
- Тихие часы — сообщение, которое ушло бы внутри окна (скажем, 22:00–08:00), ждёт конца окна по часам получателя. Пояс —
timezoneконтакта (канальные отправки) илиtimezoneрегистрации устройства (push); у кого его нет, берётся пояс приложения по умолчанию, а без него — задержки нет. - Лимит в сутки — не больше N неслужебных сообщений получателю за последние 24 часа (по контакту для каналов, по устройству для push; неудачная попытка тоже считается). Кто сверх лимита — не попадает в рассылку и считается в
total_skippedуведомления. - Местное время —
scheduled_at: "2026-09-17T10:00"сlocal_time: trueотправляет в 10:00 по часам каждого получателя: строкаscheduledна самый ранний пояс Земли (UTC+14), и доставка каждому ждёт своих 10:00, так что кампания растягивается до 26 часов. Время, прошедшее уже везде, —400;expires_atс ним не сверяется.
transactional: true отключает все три, как и category с обязательной темой (см. Контакты → Предпочтения): код входа и статус заказа никогда не задерживаются и не режутся лимитом. Отправки сценариев наследуют настройки приложения через ту же веерную рассылку.
Лучшее время отправки
send_at_best_time: true — доставка каждому получателю ждёт час, в который он обычно откликается: каждое открытие, клик и прочтение in-app за последние 90 дней — голос за час суток, в который оно случилось, по часам получателя (его timezone, иначе пояс приложения); побеждает час с наибольшим числом голосов, соседние часы считаются наполовину. Получатель с 3+ сигналами ждёт свой час; с меньшим — наследует час приложения, когда у приложения 30+ сигналов; иначе никто не ждёт и сообщение уходит сразу — так что опция безопасна с первого дня и точнее с каждой неделей. Ожидание — меньше суток: кто уже внутри своего часа, получает сейчас, остальные — когда он наступит; тихие часы приложения применяются после. Работает и при немедленной отправке, и со scheduled_at (ожидание начинается, когда отправка срабатывает), на любом канале; не сочетается с local_time (фиксированное время) и transactional (сразу) — 400. GET /api/v1/contacts/{externalId} отвечает best_send_hour: { hour, events, source: recipient | app } (null, пока неизвестно), страница контакта показывает «Лучшее время»; на странице отправки опция называется «В лучшее время каждого».
Повторяющиеся отправки
Отложенная отправка повторяется с recurrence:
{
"title": "Дайджест недели",
"body": "Что нового за неделю",
"channel_type": "email",
"target": { "segment_id": "…" },
"scheduled_at": "2026-10-05T09:00:00+03:00",
"recurrence": { "every": "week", "interval": 1, "until": "2026-12-28T00:00:00+03:00" }
}every — day, week или month; interval (по умолчанию 1; не больше 30 дней, 12 недель, 12 месяцев) задаёт шаг; until (необязательно, не дальше года от scheduled_at) — последний момент, на который может быть назначена отправка. В серии всегда одна запланированная строка: когда планировщик отправляет очередную, он создаёт следующую — та же аудитория, содержимое и параметры, свои счётчики, series_id = id первой отправки, recurrence.occurrence — её номер. Даты считаются от первой, так что ежемесячная отправка 31-го числа в короткие месяцы уходит в последний день и возвращается на 31-е; с local_time время повторяется по часам каждого получателя. Отправка, пропущенная пока сервис был недоступен, не досылается. Отмена запланированной отправки останавливает серию (DELETE /api/v1/notifications/{id} или «Отменить» на странице уведомлений); отменённая или исчерпанная серия ничего больше не создаёт. В панели — «Повторять» в блоке доставки на странице отправки, чип «↻ №n в серии» в списке.
Тест на себя
Перед рассылкой «Тест на себя» на странице отправки отправляет сообщение таким, как оно составлено — канал, переменные, варианты, HTML — на одно зарегистрированное устройство (push) или одному контакту с адресом для канала (e-mail, телефон, Telegram, MAX, VK, in-app), минуя тихие часы и лимит. Добавьте себя контактом один раз; тест появляется в списке уведомлений как обычная отправка.
A/B-тесты и цели конверсии
Отправьте два–четыре варианта заголовка и текста — и пусть получатели решат:
{
"target": { "segment_id": "…" },
"variants": [
{ "title": "Скидка 20% до конца недели", "body": "Промокод WEEK20", "weight": 1 },
{ "title": "Только до воскресенья: −20%", "body": "Промокод WEEK20", "weight": 1 }
],
"goal": { "event": "order_placed", "window_hours": 72 }
}Каждому получателю достаётся один вариант по стабильному хешу уведомления и получателя (устройство для push, контакт для канальной отправки) пропорционально весам, так что повторный fanout раздаёт те же варианты; журнал доставки помнит букву (variant). GET /api/v1/notifications/{id} (и страница уведомления) отвечает variant_stats — отправлено, доставлено, открыто, кликнуто и цель по каждому варианту — и отмечает winner: вариант, строго опережающий по кликам (при равенстве — по открытиям, потом по конверсиям). Ключи — A–D по порядку, если не заданы; веса относительные и не обязаны давать 100.
Цель — пользовательское событие (order_placed, subscription_started), которое получатель присылает в течение window_hours (по умолчанию 72, не больше 720) после своей доставки; total_converted считает таких получателей (каждого один раз), по вариантам — в variant_stats. Push-доставка находит свой контакт по связям устройства, так что user_id события должен совпадать с user_id устройства или внешним id контакта.
Досылка победителя
Добавьте ab_test — и тест решит сам:
{
"target": { "segment_id": "…" },
"variants": [{ "title": "…", "body": "…" }, { "title": "…", "body": "…" }],
"goal": { "event": "order_placed" },
"ab_test": { "share": 0.2, "window_hours": 4, "metric": "conversions" }
}Отправка уходит только тестовой когорте — доле share аудитории (0,05–0,9), выбранной по стабильному хешу уведомления и получателя, так что повторный fanout берёт тех же людей. Через window_hours (1–168) после отправки (для отложенной — после scheduled_at) планировщик выбирает победителя по metric — clicks (по умолчанию), opens или conversions, каждая как доля от отправленных; при равенстве решает общий рейтинг — и отправляет этот вариант, один, остальной аудитории: та же цель, канал, конверсионная цель и параметры, минус все, кого достиг тест. Досылка — обычное уведомление с parent_notification_id, указывающим на тест; тест отвечает ab_test (share, window_hours, metric, winner_due_at), а после решения — ab_winner_variant и ab_winner_notification_id. Страница уведомления показывает настройки под вариантами и связывает обе отправки; в панели это флажок «Досылка победителя» в блоке A/B на странице отправки. Тест, отменённый до конца окна, решать нечего; тест без измеренной вовлечённости отправляет первый вариант. Чтобы закончить тест раньше, POST /api/v1/notifications/{id}/decide (или «Выбрать победителя сейчас» на странице уведомления) принимает то же решение по счётчикам на этот момент — потом 409 already_decided.
Отправка по шаблону
Шаблоны приложения (Шаблоны в дашборде, галерея) читаются через REST и используются в отправке:
# Шаблоны с содержимым на каждом языке и {{переменными}}, которых ждут тексты
curl "https://api.pushdata.ru/api/v1/templates?channel_type=email" -H "Authorization: ApiKey pd_…"
# Отправка по шаблону: тема / текст / HTML берутся из него, переменные рендерятся один раз на всю отправку
curl -X POST https://api.pushdata.ru/api/v1/notifications \
-H "Authorization: ApiKey pd_…" -H "Content-Type: application/json" \
-d '{ "template_id": "0192…", "variables": { "orderId": "A-1042", "total": "3 990 ₽" }, "locale": "ru", "target": { "user_ids": ["u-42"] } }'template_id заполняет то, что запрос оставил пустым: тема становится заголовком (название шаблона, если темы нет), текст, html_body e-mail-шаблона; channel_type по умолчанию — тип шаблона, и противоречить ему нельзя (400); канал шаблона — канал по умолчанию, пока он активен. locale выбирает перевод на всю отправку (одна отправка рендерится один раз — для языка каждого контакта используйте сценарий). variables — значения {{плейсхолдеров}} ({{name | fallback}}, {{#if}}, {{#each}} как в сценариях); GET /api/v1/templates/{id} перечисляет их в variables. Обычная отправка тоже может нести html_body — e-mail-канал отправит его вместо обёрнутого текста. Уведомление запоминает template_id и html_body.
Журнал доставок
# По строке на получателя: исход, отметки, причина отказа, вариант A/B
curl "https://api.pushdata.ru/api/v1/notifications/0192…/deliveries?status=failed" -H "Authorization: ApiKey pd_…"GET /api/v1/notifications/{id}/deliveries отвечает { data, total }, новые сверху, страницами page / page_size (≤ 200) и с фильтром status (pending, sent, delivered, failed, clicked). В строке — user_id (ваш id пользователя: внешний id контакта или user id устройства для push), device_id / contact_id, platform, variant, sandbox, routed_via (канал, который доставил, если сработал резерв), provider_message_id, error, attempt_count и отметки sent_at / delivered_at / opened_at / clicked_at. Сам адрес (e-mail, телефон) — персональные данные, он не возвращается.
Идемпотентность
Передавайте необязательный заголовок Idempotency-Key (≤ 255 символов, уникальный в рамках приложения), чтобы повторы были безопасны: повтор с тем же ключом и телом возвращает исходный ответ с Idempotency-Replayed: true; тот же ключ с другим телом — 409 idempotency_conflict. Завершённые ключи хранятся 24 часа.
Квота
Каждый получатель расходует месячный лимит тарифа. Явный список получателей (device_ids; contact_ids или user_ids канальной отправки) больше остатка отклоняется с 402; аудиторная отправка (all, user_ids, tags для push, segment_id) урезается до остатка при веерной рассылке.
Чтение и отмена
# Одно уведомление со счётчиками (total_targets, total_sent, total_delivered, total_failed, …)
curl https://api.pushdata.ru/api/v1/notifications/0192… -H "Authorization: ApiKey pd_…"
# Отменить запланированное, ожидающее или поставленное в очередь уведомление
curl -X DELETE https://api.pushdata.ru/api/v1/notifications/0192… -H "Authorization: ApiKey pd_…"Поля в snake_case (app_id, click_action, scheduled_at, scheduled_at_local, total_sent, total_skipped, total_converted, variant_stats, recurrence, series_id, sandbox, …). Отправка из приложения в песочнице — sandbox: true: посчитана и записана, но доставлена только тестовым получателям. Отсутствующий или чужой id — 404.
Счётчики означают одно и то же везде (дашборд, App.stats, аналитика): total_sent — то, что провайдер принял, total_failed — то, что отклонил, доставляемость = total_sent / (total_sent + total_failed). total_delivered — подтверждённые доставки по колбэку устройства (эталонный service worker, iOS/Android SDK или POST /api/v1/track с event: "delivered"); у email, SMS, Telegram, MAX и in-app такого сигнала нет, поэтому для них счётчик остаётся 0 и не является показателем доставки.
Регистрация устройства
Устройство нужно зарегистрировать до того, как оно сможет получать push. Одно и то же тело работает с секретным ключом на /api/v1/devices и с публичным на /api/v1/public/devices:
curl -X POST https://api.pushdata.ru/api/v1/public/devices \
-H "Authorization: Public pk_ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ" \
-H "X-Identity-Hash: <HMAC от user-123 с вашего бэкенда>" \
-H "Content-Type: application/json" \
-d '{
"platform": "android",
"token": "fcm-registration-token",
"user_id": "user-123",
"tags": ["beta"],
"timezone": "Europe/Moscow"
}'У нового приложения включена проверка идентичности: публичный запрос с user_id несёт X-Identity-Hash — HMAC-SHA256 идентификатора пользователя, который ваш бэкенд вычисляет с секретом идентичности приложения (Проверка идентичности); без него ответ — 401 identity_unverified. Устройству без user_id хэш не нужен; с секретным ключом (/api/v1/devices) проверки нет.
Ответ 201: { "id": "0192…", "created": true } (created: false, если токен уже был известен и обновлён).
| Поле | Тип | Описание |
|---|---|---|
platform | ios | android | web | Обязательно |
transport | fcm | rustore | hms | Только Android — какой сервис выдал токен; без поля — fcm |
token | string | Токен APNs (hex, сейчас 64 символа — длина переменная), registration token FCM или URL endpoint Web Push |
user_id | string | Ваш id пользователя (≤ 512). Включает target.user_ids; должен совпадать с externalId контакта для стирания/экспорта и отписок |
tags | string | ≤ 50 тегов для target.tags |
metadata | object | Произвольные данные, ≤ 4 КБ |
timezone | string | Имя IANA |
environment | sandbox | production | Окружение APNs токена |
category | chrome | firefox | safari | edge | opera | Семейство браузера для web-устройства (дашборд группирует по нему) |
web_push_p256dh, web_push_auth | string | Обязательны для web (ключи PushSubscription в base64url) |
Один токен — одно устройство: повторная регистрация под другим user_id переводит устройство новому пользователю (прежний перестаёт получать свои push). При выходе из аккаунта деактивируйте его:
curl -X DELETE https://api.pushdata.ru/api/v1/public/devices \
-H "Authorization: Public pk_…" -H "Content-Type: application/json" \
-d '{ "token": "fcm-registration-token" }'Управление устройствами секретным ключом: GET /api/v1/devices?user_id=&platform=&status=&page=&page_size= (постранично { data, total }, токены никогда не возвращаются), PUT /api/v1/devices/:id ({ "tags": [...] }), DELETE /api/v1/devices/:id, DELETE /api/v1/devices ({ "token": … }).
События вовлечённости
Сообщайте delivered, opened и clicked с устройства. Пара (notification_id, device_id) адресует доставку; оба id есть в payload push-уведомления (pushdata_notification_id, pushdata_device_id).
curl -X POST https://api.pushdata.ru/api/v1/events \
-H "Authorization: Public pk_…" -H "Content-Type: application/json" \
-d '{ "type": "opened", "notification_id": "0192…", "device_id": "0192…" }'Ответ 202 { "accepted": true }. Серверные интеграции могут использовать POST /api/v1/track с секретным ключом и delivery_log_id либо notification_id + device_token.
Формат ошибок
Каждая ошибка — { "error": { "code", "message", "details"? } } с HTTP-статусом.
| Статус | code | Значение |
|---|---|---|
400 | invalid_argument | Некорректное тело, target или id |
401 | unauthorized | Нет или неверный API-ключ |
402 | payment_required | Лимит тарифа исчерпан или подписка просрочена |
403 | forbidden, app_inactive, ip_not_allowed | Публичный ключ на секретной операции; приложение деактивировано (владельцем или оператором) — ключи настоящие, но не обслуживаются до включения; секретный ключ с адреса вне списка разрешённых IP |
404 | not_found | Неизвестный или чужой ресурс |
409 | idempotency_conflict | Ключ повторён с другим телом |
411 | length_required | Тело без Content-Length (chunked) — API его не принимает |
413 | payload_too_large | Тело больше 1 МиБ — разбейте запрос |
415 | unsupported_media_type | Тело не application/json |
422 | validation_error | Поле не прошло валидацию или не поддерживается |
429 | rate_limited | Превышен лимит запросов (заголовки X-RateLimit-*, Retry-After) |
500 | internal_error | Внутренняя ошибка сервера |
503 | rate_limiter_unavailable | Хранилище лимитера недоступно — повторите через Retry-After |