Оглавление10 разделов
S2S postback — серверное уведомление о конверсии. Его надёжность определяется не фактом HTTP 200, а тем, можно ли доказать происхождение события, записать повтор только один раз, пережить временную недоступность получателя и восстановить историю обработки. Ниже — минимальная схема, которую можно перенести в техническое задание без домыслов.
Готовый результатКонтракт состоит из неизменяемого event_id, click_id, типа события, времени возникновения, версии схемы и статуса. Запрос подписывается HMAC, принимается в журнал до бизнес-обработки, а повторная доставка упирается в уникальный ключ, а не в надежду на единственный запрос.
Контракт события
| Поле | Тип | Обязательно | Назначение |
|---|---|---|---|
| event_id | UUID/string | да | Идемпотентный идентификатор события у отправителя |
| click_id | string | да | Связь с исходным кликом |
| event_type | enum | да | registration, ftd, approved, rejected |
| occurred_at | ISO 8601 UTC | да | Когда событие произошло |
| status_version | integer | да | Порядок изменения статуса |
| amount_minor | integer | нет | Сумма в минимальных единицах валюты |
| currency | ISO 4217 | нет | Валюта суммы |
| schema_version | integer | да | Версия контракта |
| reason_code | string | нет | Машиночитаемая причина решения |
Деньги передаются целым числом в минимальных единицах: 1250 означает 12,50 соответствующей валюты. Это исключает ошибки двоичной арифметики. occurred_at фиксирует бизнес-событие, received_at создаёт получатель, а processed_at — обработчик. Эти времена нельзя заменять одним timestamp.
Минимальная таблица входящих событий
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 и сравнивает значения без утечки времени. После ротации старый и новый ключи действуют короткое согласованное окно.
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, если в нём остались чувствительные параметры.
Идемпотентная вставка
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. Так сохраняется аудит и объясняется, почему вчерашнее начисление изменилось.
Политика повторной доставки
| Попытка | Задержка | Действие |
|---|---|---|
| 1 | сразу | Обычная доставка |
| 2 | 30 секунд | После timeout/5xx |
| 3 | 2 минуты | Экспоненциальная пауза + jitter |
| 4 | 10 минут | Повтор |
| 5 | 1 час | Повтор |
| 6 | 6 часов | Последняя автоматическая попытка |
| 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 сразу менял агрегат без входного журнала, пришлось бы просить программу повторить весь период и вручную удалять дубли.
Контроль перед запуском
- Согласовать JSON Schema и тестовые примеры.
- Проверить подпись на исходных байтах и ротацию ключей.
- Дважды отправить один event_id и получить одну запись.
- Доставить статусы не по порядку и проверить version.
- Сымитировать timeout после записи.
- Прогнать DLQ и сверить итог по click_id.
- Создать алерт на 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 закрепляются двусторонним контрактом.
Станьте партнёром и начните работать
Перейдите в партнёрскую программу, изучите актуальные условия и выберите подходящий формат сотрудничества.
СТАТЬ ПАРТНЁРОМ
