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

Контакты

Контакты (они же подписчики) — это люди, которые получают уведомления. Каждый контакт принадлежит одному приложению и идентифицируется вашим собственным 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.

bash
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

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

Поля контакта

ПолеТипОписание
externalIdstringId пользователя в вашей системе (обязателен, уникален в приложении)
namestringОтображаемое имя
emailstringEmail (нужен для канала Email)
phonestringТелефон (нужен для канала SMS); пробелы и дефисы допустимы, перед отправкой номер приводится к цифрам
maxUserIdstringId пользователя в мессенджере MAX (нужен для канала MAX); обычно заполняется сам, когда человек открывает бота по ссылке контакта (maxLink)
telegramChatIdstringЛичный чат человека с Telegram-ботом приложения (адрес личного сообщения в Telegram); только для чтения — заполняется, когда человек открывает бота по ссылке контакта (telegramLink), и очищается, когда он блокирует бота
preferenceCenterUrlstringХостируемая страница настроек контакта (только чтение, подписана) — см. ниже
localestringПредпочитаемый язык; используются шаблоны с подходящим переводом
timezonestringЧасовой пояс человека (IANA, Europe/Moscow); его читают тихие часы и отправки «в 10:00 по местному времени» — см. Ограничения отправки. Поднимается и из metadata.timezone
lastActiveAttimestampКогда человек последний раз что-то делал по данным приложения (пользовательское событие); только чтение
metadataobjectЛюбые дополнительные данные

Предпочтения и центр настроек

Отправка называет категорию (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:

html
<pushdata-preferences app-id="APP_ID" public-key="pk_…" user-id="user-123" identity-hash="…" locale="ru"></pushdata-preferences>
ts
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:

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). Страница и виджет сами записывают адрес и клиент.

bash
# Записать согласие, полученное в другом месте (бумажная форма, галочка на кассе, импорт)
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, — и это станет и триггером сценария, и условием сегмента. Один вызов с бэкенда (секретный ключ) или из приложения (публичный ключ, с хешем идентичности, если приложение проверяет пользователей):

bash
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"}'
json
{ "accepted": 1, "duplicates": 0, "workflows_started": 1 }
ПолеОписание
user_idВаш id пользователя (external_id контакта). Незнакомому пользователю создаётся контакт — в пределах лимита контактов тарифа
eventИмя: буквы, цифры, точки, двоеточия, дефисы, подчёркивания; ≤ 100 символов
propertiesЛюбой JSON-объект до 16 КиБ — payload прогона сценария и значения, которые потом читает сегмент
occurred_atISO 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:

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 условий четырёх видов:

ВидПоляОператоры
attributefield: locale, name, externalId, createdAt, lastActiveAt или metadata.<key> (вложенные ключи через точку); value — строка, число, булево или список для in. Имя хранится зашифрованным, поэтому правило по name — только exists / not_existseq, neq, contains, gt, gte, lt, lte, exists, not_exists, in
eventname; withinDays (необязательно, 1–3650); value — количество для count_*occurred, not_occurred, count_gte, count_lte
activityvalue — днейactive_within_days, inactive_for_days, never_active
channelchannel: push, email, sms, telegram, max, vkreachable (есть адрес / активное устройство), 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, GraphQL createContact / 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_mnumber1–5 — оценки давности, частоты и суммы по квинтилям самого приложения (5 = верхняя пятая часть покупателей)
rfm_segmentstringchampions (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_monetarynumberсырые значения: дней с последней покупки, покупок в окне, сумма в окне
rfm_last_purchase_atdateпоследняя покупка

Атрибуты объявляются типизированными при включении, так что редактор сегментов предлагает их сразу (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 отвечают теми же строками:

bash
curl https://api.pushdata.ru/api/v1/contacts/user-123/timeline?limit=50 \
  -H "Authorization: ApiKey pd_YOUR_SECRET_KEY"
json
{
  "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 строк за вызов.

Право на забвение и экспорт данных

graphql
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, см. Сценарии):

graphql
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 (необязателен, уникален в рамках воркфлоу) делает повторный запуск безопасным: возвращается уже начатый запуск вместо нового.