Надёжный S2S postback в арбитраже: подпись, дедупликация и повторная доставка

Готовая техническая схема S2S postback для арбитража: контракт события, HMAC-проверка, идемпотентная запись, очередь retries, SQL и разбор инцидента.

Оглавление10 разделов

S2S postback — серверное уведомление о конверсии. Его надёжность определяется не фактом HTTP 200, а тем, можно ли доказать происхождение события, записать повтор только один раз, пережить временную недоступность получателя и восстановить историю обработки. Ниже — минимальная схема, которую можно перенести в техническое задание без домыслов.

Готовый результат

Контракт состоит из неизменяемого event_id, click_id, типа события, времени возникновения, версии схемы и статуса. Запрос подписывается HMAC, принимается в журнал до бизнес-обработки, а повторная доставка упирается в уникальный ключ, а не в надежду на единственный запрос.

Контракт события

Поля postback v1
ПолеТипОбязательноНазначение
event_idUUID/stringдаИдемпотентный идентификатор события у отправителя
click_idstringдаСвязь с исходным кликом
event_typeenumдаregistration, ftd, approved, rejected
occurred_atISO 8601 UTCдаКогда событие произошло
status_versionintegerдаПорядок изменения статуса
amount_minorintegerнетСумма в минимальных единицах валюты
currencyISO 4217нетВалюта суммы
schema_versionintegerдаВерсия контракта
reason_codestringнетМашиночитаемая причина решения

Деньги передаются целым числом в минимальных единицах: 1250 означает 12,50 соответствующей валюты. Это исключает ошибки двоичной арифметики. occurred_at фиксирует бизнес-событие, received_at создаёт получатель, а processed_at — обработчик. Эти времена нельзя заменять одним timestamp.

Минимальная таблица входящих событий

PostgreSQL: журнал postback
CREATE TABLE postback_events (
  sender_id       text        NOT NULL,
  event_id        text        NOT NULL,
  click_id        text        NOT NULL,
  event_type      text        NOT NULL,
  status_version  integer     NOT NULL,
  occurred_at     timestamptz NOT NULL,
  received_at     timestamptz NOT NULL DEFAULT now(),
  payload         jsonb       NOT NULL,
  payload_sha256  text        NOT NULL,
  process_status  text        NOT NULL DEFAULT 'new',
  process_error   text,
  PRIMARY KEY (sender_id, event_id)
);
CREATE INDEX postback_by_click ON postback_events (click_id, occurred_at);

Каноническая подпись HMAC

Отправитель подписывает байты тела запроса, а не повторно собранный JSON: перестановка полей или пробелов меняет подпись. В заголовках передаются идентификатор ключа, Unix-время и hex/base64-подпись. Получатель сначала ограничивает размер тела и допустимое отклонение времени, затем вычисляет HMAC и сравнивает значения без утечки времени. После ротации старый и новый ключи действуют короткое согласованное окно.

Node.js: проверка HMAC-SHA256
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, signatureHex, timestamp, secret) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(String(timestamp)).update('.').update(rawBody).digest();
  const supplied = Buffer.from(signatureHex, 'hex');
  return supplied.length === expected.length &&
    timingSafeEqual(supplied, expected);
}

Проверка timestamp уменьшает окно повторного проигрывания подписанного запроса, но не заменяет дедупликацию event_id. Секрет хранится вне репозитория и логов. В журнал нельзя писать заголовок авторизации или полный URL, если в нём остались чувствительные параметры.

Идемпотентная вставка

PostgreSQL: принять повтор без дубля
INSERT INTO postback_events
  (sender_id,event_id,click_id,event_type,status_version,occurred_at,payload,payload_sha256)
VALUES
  ($1,$2,$3,$4,$5,$6,$7,$8)
ON CONFLICT (sender_id,event_id) DO NOTHING
RETURNING event_id;

Пустой RETURNING означает уже известное событие. Получатель отвечает 2xx и на новый, и на корректный повтор: иначе отправитель будет доставлять его бесконечно. Если тот же event_id пришёл с другим hash, запись не перезаписывают — создают security-сигнал о конфликте контракта.

Статус как история, а не перезапись

FTD может пройти цепочку pending → approved либо pending → rejected. Храните каждую версию с уникальностью по event_id и status_version. Обработчик применяет только версию выше текущей; поздно пришедший status_version=2 не должен отменить уже применённый version=3. Так сохраняется аудит и объясняется, почему вчерашнее начисление изменилось.

Политика повторной доставки

Пример расписания retries
ПопыткаЗадержкаДействие
1сразуОбычная доставка
230 секундПосле timeout/5xx
32 минутыЭкспоненциальная пауза + jitter
410 минутПовтор
51 часПовтор
66 часовПоследняя автоматическая попытка
DLQпосле лимитаРучная/пакетная повторная обработка

На 4xx безусловно повторять нельзя: 401 требует проверки ключа, 400 — контракта, 413 — размера. Для 429 учитывают Retry-After. Timeout считается неопределённым результатом: сервер мог записать событие и не успеть ответить, поэтому следующий запрос обязан быть идемпотентным.

SQL для ежедневной сверки

Потери и задержки по типам событий
SELECT
  date_trunc('hour', occurred_at) AS event_hour,
  event_type,
  count(*) AS events,
  percentile_cont(0.95) WITHIN GROUP
    (ORDER BY extract(epoch FROM received_at-occurred_at)) AS p95_lag_sec,
  count(*) FILTER (WHERE process_status='error') AS errors
FROM postback_events
WHERE occurred_at >= now() - interval '24 hours'
GROUP BY 1,2 ORDER BY 1,2;

Обезличенный инцидент

В 14:00 доля approved в трекере упала, хотя кабинет программы продолжал расти. Запрос показал нормальное число входящих approved, но 38% строк имели process_status=error. Все ошибки появились после новой версии reason_code, которую строгий enum не принимал. Команда добавила неизвестное значение в карантин, повторно обработала event_id из DLQ и восстановила отчёт без повторных выплат. Если бы postback сразу менял агрегат без входного журнала, пришлось бы просить программу повторить весь период и вручную удалять дубли.

Контроль перед запуском

  1. Согласовать JSON Schema и тестовые примеры.
  2. Проверить подпись на исходных байтах и ротацию ключей.
  3. Дважды отправить один event_id и получить одну запись.
  4. Доставить статусы не по порядку и проверить version.
  5. Сымитировать timeout после записи.
  6. Прогнать DLQ и сверить итог по click_id.
  7. Создать алерт на p95 задержки, ошибки и конфликт hash.

Источники и дата проверки

Технические ссылки проверены 1 августа 2026 года: Node.js Crypto API — https://nodejs.org/api/crypto.html; PostgreSQL INSERT ... ON CONFLICT — https://www.postgresql.org/docs/current/sql-insert.html. Конкретные алгоритм подписи, окно времени и SLA закрепляются двусторонним контрактом.

Станьте партнёром и начните работать

Перейдите в партнёрскую программу, изучите актуальные условия и выберите подходящий формат сотрудничества.

СТАТЬ ПАРТНЁРОМ

Связанные материалы

Все статьи
Рекламные технологии

Ретаргетинг в affiliate-маркетинге: сегменты, исключения и проверка инкрементальности

Как проектировать ретаргетинг: события членства, окна, suppression, последовательности сообщений, частота, атрибуция, privacy и проверка дополнительного эффекта.

Рекламные технологии

Частота показа и накопленный охват: как не спутать насыщение аудитории с плохим креативом

Как анализировать frequency и cumulative reach: распределение контактов, когорты первого показа, предельный эффект и отличие насыщения аудитории от усталости креатива.

Рекламные технологии

Передача ценности конверсии в рекламную платформу: как не обучить алгоритм на шуме

Как подготовить conversion value для рекламной платформы: событие, зрелость, корректировки, cap выбросов, версии модели, backtest и безопасный rollout.