Контакты
Контакты (они же подписчики) — это люди, которые получают уведомления. Каждый контакт принадлежит одному приложению и идентифицируется вашим собственным externalId. Контактами управляют через REST (/api/v1/contacts, ниже) и GraphQL API (дашборд использует те же операции).
Как связаны контакты и устройства
Устройство, зарегистрированное с user_id: "user-123", принадлежит контакту с externalId = user-123 в том же приложении. Эта единственная связь определяет всё:
target.user_idsв REST-отправке доставляет на устройства этих контактов;- канальные отправки (email / SMS / MAX / in-app) берут адрес из контакта;
- отписки (
updateContactPreference) действуют на каждый канал, включая push; eraseContact(право на забвение) иexportSubscriberDataохватывают устройства контакта.
Используйте один и тот же идентификатор с обеих сторон.
Создание контакта
Дашборд
Откройте Приложения → Контакты → Новый контакт и заполните поля.
REST
POST /api/v1/contacts — идемпотентный upsert по external_id: первый вызов создаёт контакт (201), повторный — обновляет переданные поля (200); поля, которых нет в теле, не трогаются, metadata заменяется целиком. Поля: external_id, name, email, phone, max_user_id, locale, metadata.
curl -X POST https://api.pushdata.ru/api/v1/contacts \
-H "Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"external_id":"user-123","email":"user@example.com","name":"Иван Петров","phone":"+79991234567","locale":"ru","metadata":{"plan":"pro"}}'GET /api/v1/contacts/user-123 возвращает контакт, DELETE /api/v1/contacts/user-123 стирает его вместе с устройствами, настройками и inbox (то же, что eraseContact). Заявки из форм Tilda превращаются в контакты приёмником POST /api/v1/integrations/tilda (см. Интеграции); серверные SDK делают всё это методами upsertContact / getContact / deleteContact.
GraphQL
curl -X POST https://api.pushdata.ru/api/graphql \
-H "Authorization: ApiKey pd_ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation($input: CreateContactInput!) { createContact(input: $input) { id externalId email } }",
"variables": { "input": { "appId": "APP_ID", "externalId": "user-123", "email": "user@example.com", "name": "Иван Петров", "phone": "+79991234567", "locale": "ru" } }
}'Пара (appId, externalId) уникальна: создание уже существующего контакта возвращает ошибку 409 (conflict). Существующий контакт меняется через updateContact(id, input).
Поля контакта
| Поле | Тип | Описание |
|---|---|---|
externalId | string | Id пользователя в вашей системе (обязателен, уникален в приложении) |
name | string | Отображаемое имя |
email | string | Email (нужен для канала Email) |
phone | string | Телефон (нужен для канала SMS); пробелы и дефисы допустимы, перед отправкой номер приводится к цифрам |
maxUserId | string | Id пользователя в мессенджере MAX (нужен для канала MAX); обычно заполняется сам, когда человек открывает бота по ссылке контакта (maxLink) |
telegramChatId | string | Личный чат человека с Telegram-ботом приложения (адрес личного сообщения в Telegram); только для чтения — заполняется, когда человек открывает бота по ссылке контакта (telegramLink), и очищается, когда он блокирует бота |
preferenceCenterUrl | string | Хостируемая страница настроек контакта (только чтение, подписана) — см. ниже |
locale | string | Предпочитаемый язык; используются шаблоны с подходящим переводом |
timezone | string | Часовой пояс человека (IANA, Europe/Moscow); его читают тихие часы и отправки «в 10:00 по местному времени» — см. Ограничения отправки. Поднимается и из metadata.timezone |
lastActiveAt | timestamp | Когда человек последний раз что-то делал по данным приложения (пользовательское событие); только чтение |
metadata | object | Любые дополнительные данные |
Предпочтения и центр настроек
Отправка называет категорию (category в REST- и GraphQL-отправках, у SEND-шага воркфлоу). Что человек может с ней сделать, описывает каталог тем приложения (Приложения → Темы): ключ темы (key) — это категория, name и description видит человек, а умолчание решает, что получает контакт без своего выбора:
| Умолчание | Смысл |
|---|---|
OPTED_IN | Получают все, пока не выключат, — классическая отписка. Категория без темы ведёт себя так же. |
OPTED_OUT | Не получает никто, пока не включит, — дайджест по запросу. |
REQUIRED | Доставляется всегда и не выключается, даже после отписки от всего, — только транзакционные и сервисные сообщения. |
Тему можно ограничить каналами (channelTypes); иначе она относится ко всем каналам, по которым приложение достаёт человека. Фан-аут учитывает всё это на любом пути (API-отправки, отложенные отправки, шаги воркфлоу): тема OPTED_OUT доходит только до контактов с явным включением, REQUIRED игнорирует отписки, OPTED_IN пропускает тех, кто её выключил. Ссылка отписки в футере письма пишет для канала категорию * — все необязательные темы для него выключены, пока человек не включит одну из них обратно.
Хостируемая страница настроек
У каждого контакта есть личная страница /p/<подписанный токен> на хосте API — preferenceCenterUrl в GraphQL, preference_center_url в REST, строка «Страница настроек» на карточке контакта в дашборде и ссылка «Notification preferences» в футере каждого письма рядом с «Unsubscribe». Она показывает темы × каналы флажками, в цветах приложения (Приложения → Настройки → акцентный цвет и логотип), по-русски или по-английски по языку браузера, и работает без JavaScript. Токен подписан и не истекает — как и ссылка отписки, граница доверия здесь почтовый ящик человека.
Встраиваемый компонент
Для страницы профиля внутри вашего приложения web SDK даёт <pushdata-preferences> (import 'pushdata/preferences') и client.preferences.get / set — публичный ключ плюс хеш идентичности, как у inbox:
<pushdata-preferences app-id="APP_ID" public-key="pk_…" user-id="user-123" identity-hash="…" locale="ru"></pushdata-preferences>const prefs = await client.preferences.get('user-123') // { channels, unsubscribed, topics: [{ key, name, default, required, preferences: { email: true, … } }] }
await client.preferences.set('user-123', { topic: 'marketing', channelType: 'email', enabled: false })Под капотом: GET /api/v1/public/preferences?user_id= и PUT /api/v1/public/preferences ({ user_id, topic, channel_type, enabled } или список preferences). У пользователя без контакта он появится при первой записи, так что выбор, сделанный до первой отправки, сохраняется.
API
Ваш бэкенд читает и пишет от имени контакта секретным ключом — GET /api/v1/contacts/{external_id}/preferences и PUT /api/v1/contacts/{external_id}/preferences с тем же телом — или через GraphQL:
mutation {
updateContactPreference(input: { subscriberId: "CONTACT_ID", category: "marketing", channelType: EMAIL, enabled: false }) {
id
}
}Выключить тему REQUIRED нельзя нигде (400 topic_required); включение темы снимает отписку от всего канала, чтобы переключатель никогда не выглядел «не сработавшим».
Реестр согласий
Антиспам-правила с 1 августа 2025 требуют, чтобы массовая SMS опиралась на явное документированное согласие абонента — дата, способ, текст, — которое компания обязана показать по запросу. PushData ведёт этот реестр: каждая подписка человека (хостируемая страница настроек, виджет <pushdata-preferences>, ваш API, импорт, переключатель в панели) становится записью согласия с доказательством, а отписка закрывает её (revoked_at, revoked_source). Страница и виджет сами записывают адрес и клиент.
# Записать согласие, полученное в другом месте (бумажная форма, галочка на кассе, импорт)
curl -X POST https://api.pushdata.ru/api/v1/contacts/user-42/consents \
-H "Authorization: ApiKey pd_…" -H "Content-Type: application/json" \
-d '{ "purpose": "marketing", "channel": "sms", "source": "form",
"evidence": { "ip": "93.184.216.34", "url": "https://shop.example/checkout", "text": "Согласен получать SMS о скидках", "documentVersion": "2026-09" },
"granted_at": "2026-09-18T10:00:00+03:00" }'
# Реестр контакта, новое сверху; реестр всего приложения для регулятора (JSON или CSV)
curl https://api.pushdata.ru/api/v1/contacts/user-42/consents -H "Authorization: ApiKey pd_…"
curl "https://api.pushdata.ru/api/v1/consents?since=2026-01-01T00:00:00Z&channel=sms&format=csv" -H "Authorization: ApiKey pd_…"
# Отозвать
curl -X DELETE https://api.pushdata.ru/api/v1/contacts/user-42/consents/0192… -H "Authorization: ApiKey pd_…"purpose — ключ темы, marketing или transactional; channel — имя канала или отсутствует (все каналы); source — api (по умолчанию), import, form, widget, preference_center, double_opt_in или dashboard; evidence — любой объект до 4 КБ. Повторное такое же согласие отвечает существующей записью (200), новое — 201. POST /api/v1/contacts принимает consents: [ … ] (до 20) вместе с upsert — для импорта. Страница контакта показывает реестр и даёт оператору записать или отозвать согласие; лента перечисляет каждое согласие и отзыв; выгрузка данных включает реестр; удаление контакта удаляет и его.
Проверка. В настройках приложения (Соответствие закону → Требовать согласие; consentRequiredChannels в GraphQL) отметьте каналы, для которых нужно согласие — SMS для правил выше. Неслужебная отправка через такой канал доходит только до контактов с действующим согласием на её category (тему) или на marketing; остальные считаются в total_skipped. Шаги SEND сценариев пропускают их так же. Push не проверяется (согласие — разрешение на устройстве).
Пользовательские события
Расскажите PushData, что делает человек — order_placed, cart_abandoned, level_up, — и это станет и триггером сценария, и условием сегмента. Один вызов с бэкенда (секретный ключ) или из приложения (публичный ключ, с хешем идентичности, если приложение проверяет пользователей):
curl -X POST https://api.pushdata.ru/api/v1/track/events \
-H "Authorization: ApiKey pd_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id":"user-123","event":"order_placed","properties":{"total":1290,"currency":"RUB"},"idempotency_key":"order-1001"}'{ "accepted": 1, "duplicates": 0, "workflows_started": 1 }| Поле | Описание |
|---|---|
user_id | Ваш id пользователя (external_id контакта). Незнакомому пользователю создаётся контакт — в пределах лимита контактов тарифа |
event | Имя: буквы, цифры, точки, двоеточия, дефисы, подчёркивания; ≤ 100 символов |
properties | Любой JSON-объект до 16 КиБ — payload прогона сценария и значения, которые потом читает сегмент |
occurred_at | ISO 8601; по умолчанию — сейчас |
idempotency_key | Повторная отправка с тем же ключом — no-op (duplicates) |
Несколько сразу — events: [ … ] (≤ 100). Каждое событие обновляет последнюю активность контакта (lastActiveAt) и запускает активный сценарий приложения, у которого идентификатор триггера равен имени события (см. Сценарии); payload прогона — свойства плюс event и occurred_at. Клиентские SDK оборачивают вызов: client.events.track(userId, 'order_placed', { total: 1290 }) в вебе и React Native, trackEvent во Flutter, trackEvent / track_event в серверных SDK.
Сегменты
Сегмент — сохранённый набор правил по контактам приложения: «заказывали за последние 30 дней», «не заходили 14 дней», «есть e-mail и локаль ru». Правила вычисляются в момент отправки, так что сегмент — это всегда те, кто подходит сейчас. Создавайте их в дашборде (Сегменты) со счётчиком в реальном времени или через GraphQL:
mutation CreateSegment {
createSegment(input: {
appId: "APP_ID"
name: "Активные покупатели"
rules: {
match: "all"
conditions: [
{ kind: "event", name: "order_placed", op: "occurred", withinDays: 30 },
{ kind: "channel", channel: "email", op: "reachable" },
{ kind: "attribute", field: "metadata.plan", op: "in", value: ["pro", "team"] }
]
}
}) { id size }
}
query Segments { segments(appId: "APP_ID") { id name size rules } }
query SegmentPreview { segmentPreview(appId: "APP_ID", rules: { match: "any", conditions: [{ kind: "activity", op: "inactive_for_days", value: 14 }] }) }rules — это { match: "all" | "any", conditions: [ … ] }, до 20 условий четырёх видов:
| Вид | Поля | Операторы |
|---|---|---|
attribute | field: locale, name, externalId, createdAt, lastActiveAt или metadata.<key> (вложенные ключи через точку); value — строка, число, булево или список для in. Имя хранится зашифрованным, поэтому правило по name — только exists / not_exists | eq, neq, contains, gt, gte, lt, lte, exists, not_exists, in |
event | name; withinDays (необязательно, 1–3650); value — количество для count_* | occurred, not_occurred, count_gte, count_lte |
activity | value — дней | active_within_days, inactive_for_days, never_active |
channel | channel: push, email, sms, telegram, max, vk | reachable (есть адрес / активное устройство), unreachable |
Числа в metadata сравниваются как числа ({ field: "metadata.orders", op: "gte", value: 5 }), даты — как моменты времени; email и phone зашифрованы, поэтому правило может спросить, достижим ли контакт по каналу, но не какой у него адрес. Отправка в сегмент — target: { segment_id } в REST, segmentId в sendNotification / scheduleNotification / audienceEstimate или выбор на странице отправки: для push аудитория — устройства подходящих контактов (по тем же связям, что и contact_ids), для канальной отправки — сами контакты. Удаление сегмента не трогает отправленные в него уведомления; запланированная отправка в удалённый сегмент никого не достигнет.
Типизированные атрибуты
Приложение может объявить ключи metadata, на которые опирается, с типом — Настройки → Атрибуты контактов (App.contactAttributes / updateApp в GraphQL): string, number, boolean или date, с необязательной подписью. Объявленный ключ:
- приводится и проверяется при каждой записи — REST
POST /api/v1/contacts, GraphQLcreateContact/updateContact: строка"42"становится числом 42,"true"— булевым,"2026-01-15"— моментом2026-01-15T00:00:00.000Z; значение, которое привести нельзя, —400с именем ключа (metadata.orders must be a number); - сравнивается по типу в сегментах — ключ
dateс датой или моментом, ключnumberс числом даже если правило набрано строкой, ключbooleanсtrue/false; строки, записанные до объявления ключа с чем-то другим, просто не подходят; - подсказывается в редакторе правил — поле ключа metadata предлагает объявленные ключи и показывает их тип.
Необъявленные ключи остаются свободными, как раньше. Типизируются только ключи верхнего уровня (metadata.<key>, не вложенные пути).
«Вход в сегмент» как триггер
Сценарий, чей триггер — сегмент (Сценарии → Новый → Что запускает сценарий → Контакт попал в сегмент; идентификатор — segment:<id сегмента>), запускает прогон для каждого контакта, который начинает подходить под правила: «стал постоянным», «не активен 30 дней», «зарегистрировался до акции». PushData хранит состав таких сегментов: запись контакта (upsert, событие) сразу переоценивает сегменты для него, а периодический проход (раз в 5 минут) ловит правила, которые меняются со временем. Контакты, которые уже внутри при включении триггера, не считаются вошедшими — первая оценка записывает их молча; контакт, который вышел и вернулся, входит снова (новый прогон). Payload прогона несёт segment_id, segment_name и entered_at; шаг FILTER тоже умеет спросить «контакт в сегменте?».
RFM-атрибуты
Давность, частота и сумма покупок человека — атрибутами; простая арифметика по пользовательским событиям одного имени, несущим сумму. Включается в Приложения → Настройки → RFM-атрибуты: событие покупки (purchase по умолчанию), свойство с суммой (amount), окно в днях (365). Дальше PushData держит на каждом контакте с покупкой в окне:
| Атрибут | Тип | Значение |
|---|---|---|
rfm_r, rfm_f, rfm_m | number | 1–5 — оценки давности, частоты и суммы по квинтилям самого приложения (5 = верхняя пятая часть покупателей) |
rfm_segment | string | champions (R≥4, F≥4, M≥4), loyal (F≥4), new (R≥4, F≤2), at_risk (R≤2, F≥3), lost (R≤2), potential (остальные) |
rfm_recency_days, rfm_frequency, rfm_monetary | number | сырые значения: дней с последней покупки, покупок в окне, сумма в окне |
rfm_last_purchase_at | date | последняя покупка |
Атрибуты объявляются типизированными при включении, так что редактор сегментов предлагает их сразу (metadata.rfm_segment = at_risk, metadata.rfm_monetary > 10000), а триггер «вход в сегмент» запускает сценарий возврата в момент, когда покупатель сползает в at_risk. Событие покупки пересчитывает своего покупателя сразу по порогам последнего полного прохода; полный проход приложения (свежие пороги, все покупатели) — раз в сутки и по кнопке Пересчитать сейчас; GraphQL recomputeRfm(appId). У контакта без покупки в окне RFM-атрибутов нет.
Когорты и удержание
Возвращаются ли те, кто начал, — по неделям, на странице аналитики (Аналитика → Когорты и удержание, выбрано одно приложение). Когорта — неделя (понедельник–воскресенье, UTC) первого пользовательского события контакта; в ячейках — сколько контактов когорты сообщили событие снова через 1, 2, … недель, долей от когорты. «Любое событие» измеряет активность вообще; выбор одного события (purchase) делает и первое событие, и возвраты только этим событием — «первая покупка → купил снова». Окно — 4, 8 или 12 недель; контакт, активный до окна, новой когортой не считается. Таблица выгружается в CSV (разделитель ;, UTF-8 с BOM для Excel). GraphQL: retentionCohorts(appId, event, weeks) { cohorts { week size counts retained } events }.
Лента контакта
Всё, что PushData знает об одном человеке, в одной хронологии, новое сверху: каждая доставка с отметками и — если не ушла — причиной от провайдера, in-app-сообщения и их прочтения, пользовательские события, прогоны сценариев и чем закончились (причина фильтра, ошибка), изменения настроек, регистрации устройств. Её показывает страница контакта; API отвечают теми же строками:
curl https://api.pushdata.ru/api/v1/contacts/user-123/timeline?limit=50 \
-H "Authorization: ApiKey pd_YOUR_SECRET_KEY"{
"items": [
{ "at": "2026-09-16T10:02:11.000Z", "kind": "delivery_failed", "title": "Заказ ORD-001", "detail": "SMTP 550 mailbox unavailable", "channel_type": "email", "notification_id": "…", "sandbox": false, "routed_via": null, "variant": null },
{ "at": "2026-09-16T10:00:00.000Z", "kind": "event", "title": "order_placed", "detail": "{\"total\":1290}", "event_id": "…" }
]
}kind — одно из delivery_sent, delivery_failed, delivery_delivered, delivery_opened, delivery_clicked, inbox_message, inbox_read, event, workflow_started, workflow_completed, workflow_failed, workflow_filtered, workflow_cancelled, preference_opt_out, preference_opt_in, device_registered; строка доставки также говорит, была ли она симулирована в песочнице, доставлена резервным каналом (routed_via) и какой A/B-вариант несла. GraphQL: contactTimeline(contactId, limit). До 200 строк за вызов.
Право на забвение и экспорт данных
mutation EraseContact { eraseContact(contactId: "CONTACT_ID") { contactDeleted devicesDeleted deliveryLogsScrubbed } }
query ExportSubscriberData { exportSubscriberData(contactId: "CONTACT_ID") }eraseContact доступен только владельцу и необратим: удаляет контакт, его устройства (в том числе связанные только через user_id), предпочтения и in-app сообщения, а журналы доставки и выполнения воркфлоу анонимизирует. exportSubscriberData возвращает всё, что хранится о субъекте, в JSON.
Контакты в воркфлоу
Передайте PushData-id контакта как subscriberId при запуске воркфлоу (в REST — POST /api/v1/workflows/:id/trigger с contact_external_id или contact_id, см. Сценарии):
mutation {
triggerWorkflow(input: {
workflowId: "019c9a99-..."
subscriberId: "019c9359-..."
payload: { orderId: "ORD-001" }
idempotencyKey: "order-ORD-001-placed"
}) {
id
status
}
}Воркер воркфлоу определяет получателя для каждого SEND-шага по типу канала: email для Email, phone для SMS, maxUserId для MAX, telegramChatId для Telegram (чат канала, если контакт не подключил бота), сам контакт для in-app и устройства контакта (по externalId) для push. idempotencyKey (необязателен, уникален в рамках воркфлоу) делает повторный запуск безопасным: возвращается уже начатый запуск вместо нового.