PD Документация ← На главную

Уведомления

Уведомления — это сообщения, которые доставляются на устройства через 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.

Отправка уведомления

bash
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:

json
{ "id": "0192…", "status": "queued" }

status равен queued для немедленной отправки (веерная рассылка выполняется асинхронно) и scheduled, если задан scheduled_at.

Тело запроса

ПолеТипОбязательноОписание
titlestring✓Заголовок (≤ 1024 символов)
bodystring✓Текст (≤ 4096 символов)
targetobject✓Ровно один из ключей ниже (для канальной отправки — см. следующий раздел)
target.alltrueВсе активные устройства приложения
target.device_idsstringКонкретные id устройств (≤ 10 000)
target.user_idsstringУстройства, зарегистрированные с этими user_id
target.tagsstringУстройства, у которых в tags есть любой из этих тегов
dataobjectПроизвольные ключ-значение, ≤ 4 КБ в сериализованном виде. Доставляется как есть; сервер добавляет pushdata_notification_id / pushdata_device_id для трекинга
click_actionstringURL / deep link, открываемый по нажатию (deep_link принимается как синоним)
iconstringURL иконки (Android, Web Push)
image / image_urlstringURL большого изображения (FCM, Web Push; в APNs передаётся как pushdata_image_url для service extension)
badgenumberСчётчик на иконке (iOS)
soundstringИмя звука (по умолчанию "default")
scheduled_atISO 8601Отложенная отправка; до этого момента статус scheduled. С local_time — время без пояса YYYY-MM-DDTHH:MM
local_timeboolean«В это время по часам каждого получателя» — см. Ограничения отправки
recurrenceobject{ every: day | week | month, interval?, until? } — та же отправка снова начиная со scheduled_at, см. Повторяющиеся отправки
transactionalbooleanСлужебная отправка: без тихих часов и лимита в сутки (код, статус заказа). category с обязательной темой освобождена сама по себе
send_at_best_timebooleanКаждый получатель ждёт свой лучший час — см. Лучшее время отправки. Не сочетается с local_time и transactional
expires_atISO 8601TTL push-уведомления: после этого момента не доставляется (APNs expiration, FCM ttl, Web Push TTL)
variantsobjectA/B-тест: 2–4 { key?, title, body, weight? } — см. A/B-тесты и цели. title / body тогда можно не передавать
goalobject{ event, window_hours? } — цель конверсии, см. ниже
ab_testobject{ 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.

bash
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_typepush | email | sms | telegram | max | in_appБез поля — push. Регистр не важен
channel_idstringКонкретный канал приложения; без него берётся самый ранний активный канал этого типа. Неизвестный или чужой — 404, выключенный — 400
categorystringКатегория подписки (≤ 100 символов): контакты, отключившие её для этого канала в настройках, пропускаются
target.contact_idsstringPushData-id контактов (≤ 10 000); только для канальных отправок
target.segment_idstringСохранённый сегмент: те, кто подходит под его правила в момент отправки — для 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 в журнале доставок и в вебхуках, а затем открывает адрес. В блок данных текста сообщения кнопки не попадают.

bash
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:

json
{
  "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-тесты и цели конверсии

Отправьте два–четыре варианта заголовка и текста — и пусть получатели решат:

json
{
  "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 — и тест решит сам:

json
{
  "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 и используются в отправке:

bash
# Шаблоны с содержимым на каждом языке и {{переменными}}, которых ждут тексты
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.

Журнал доставок

bash
# По строке на получателя: исход, отметки, причина отказа, вариант 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) урезается до остатка при веерной рассылке.

Чтение и отмена

bash
# Одно уведомление со счётчиками (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:

bash
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, если токен уже был известен и обновлён).

ПолеТипОписание
platformios | android | webОбязательно
transportfcm | rustore | hmsТолько Android — какой сервис выдал токен; без поля — fcm
tokenstringТокен APNs (hex, сейчас 64 символа — длина переменная), registration token FCM или URL endpoint Web Push
user_idstringВаш id пользователя (≤ 512). Включает target.user_ids; должен совпадать с externalId контакта для стирания/экспорта и отписок
tagsstring≤ 50 тегов для target.tags
metadataobjectПроизвольные данные, ≤ 4 КБ
timezonestringИмя IANA
environmentsandbox | productionОкружение APNs токена
categorychrome | firefox | safari | edge | operaСемейство браузера для web-устройства (дашборд группирует по нему)
web_push_p256dh, web_push_authstringОбязательны для web (ключи PushSubscription в base64url)

Один токен — одно устройство: повторная регистрация под другим user_id переводит устройство новому пользователю (прежний перестаёт получать свои push). При выходе из аккаунта деактивируйте его:

bash
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).

bash
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Значение
400invalid_argumentНекорректное тело, target или id
401unauthorizedНет или неверный API-ключ
402payment_requiredЛимит тарифа исчерпан или подписка просрочена
403forbidden, app_inactive, ip_not_allowedПубличный ключ на секретной операции; приложение деактивировано (владельцем или оператором) — ключи настоящие, но не обслуживаются до включения; секретный ключ с адреса вне списка разрешённых IP
404not_foundНеизвестный или чужой ресурс
409idempotency_conflictКлюч повторён с другим телом
411length_requiredТело без Content-Length (chunked) — API его не принимает
413payload_too_largeТело больше 1 МиБ — разбейте запрос
415unsupported_media_typeТело не application/json
422validation_errorПоле не прошло валидацию или не поддерживается
429rate_limitedПревышен лимит запросов (заголовки X-RateLimit-*, Retry-After)
500internal_errorВнутренняя ошибка сервера
503rate_limiter_unavailableХранилище лимитера недоступно — повторите через Retry-After