Постбэк (S2S конверсии)
Серверный postback MeerPartners: как сообщить платформе о конверсии. Поля запроса, идемпотентность по external_order_id, ответы и коды отклонения. Примеры на cURL, Node.js и Python.
Postback — это серверное уведомление от вашего бэкенда платформе о том, что у вас произошла конверсия: продажа, лид, регистрация, возврат или чарджбэк. Это главный механизм интеграции для бизнеса: именно по postback платформа создаёт конверсию, атрибутирует её партнёру и начисляет комиссию.
/api/v1/postback🔒 HMACЗапрос подписывается HMAC по секрету API-ключа со скоупом postback. Алгоритм подписи и заголовки (X-Api-Key-Id, X-Timestamp, X-Signature, опционально X-Tenant-Id) — на странице API-ключи и подпись. Без валидной подписи запрос не обрабатывается.
Сначала — клик
Чтобы конверсию было кому атрибутировать, у вас должен быть click_id пользователя. Его выдаёт трекинг-ссылка партнёра как aff_click при редиректе. Сохраните его рядом с заказом и передайте в postback как ref. См. Трекинг-ссылки и макросы.
Поля запроса
Тело — JSON. Content-Type: application/json.
external_order_idstringrequiredУникальный ID заказа/события в вашей системе (1–128 символов). Это ключ идемпотентности и дедупликации — по нему платформа отличает повтор от нового события.
event_typestringrequiredТип события: sale (продажа), lead (лид), registration (регистрация), refund (возврат), chargeback (чарджбэк).
refstringoptionalАтрибуция: click_id (значение aff_click из редиректа) или code трекинг-ссылки. Обязателен для sale/lead/registration; для refund/chargeback не нужен (событие находит исходную конверсию по external_order_id).
sub_idstringoptionalСквозная метка партнёра (до 128 символов), если вы её прокидывали и хотите сохранить в конверсии.
amountstringoptionalСумма заказа строкой (чтобы не терять точность), например "19.99". Обязательна для sale вместе с currency.
currencystringoptionalВалюта суммы, ISO-4217, 3 буквы — "USD", "RUB". Обязательна для sale.
statusstringoptionalapproved или pending. Влияет на старт конверсии; по умолчанию конверсия стартует в pending и проходит hold-период.
occurred_atstringoptionalВремя события на вашей стороне в формате RFC 3339 (2026-06-01T10:15:00Z). По умолчанию — момент приёма.
metaobjectoptionalПроизвольные данные (план, промокод и т.п.). Сохраняются в конверсии для аналитики и разбора.
Обязательность зависит от event_type
Для sale нужны external_order_id, event_type, ref, amount, currency. Для lead/registration — external_order_id, event_type, ref (сумма необязательна). Для refund/chargeback — только external_order_id и event_type. Если обязательного поля нет — POSTBACK_MISSING_FIELD (422).
Идемпотентность
Платформа дедуплицирует события по паре (ваш tenant, external_order_id) и типу события — под advisory-lock, поэтому даже гонка двух одинаковых postback не создаст две конверсии.
- Первый успешный postback создаёт конверсию и комиссию.
- Повтор с тем же
external_order_id→ тот же результат и"duplicate": true, без побочных эффектов.
Это значит, что ретраить безопасно: повторная отправка не задвоит начисление.
Ответы
Ответ — «голый» JSON-объект (без обёртки data). HTTP-код 200 при успешной или дублирующей обработке.
{
"accepted": true,
"duplicate": false,
"conversion_id": 7741,
"commission": {
"id": 7741,
"amount": "2.50",
"currency": "USD",
"status": "pending",
"hold_until": "2026-06-15T10:15:00Z"
}
}{ "accepted": true, "duplicate": true, "conversion_id": 7741 }{ "accepted": false, "reason": "POSTBACK_UNKNOWN_REF" }| Поле ответа | Значение |
|---|---|
accepted | true, если событие принято в обработку; false для органики/неприменимого refund |
duplicate | true, если это повтор уже обработанного external_order_id |
conversion_id | ID созданной (или найденной) конверсии |
commission | Краткая инфо по комиссии нулевого уровня: id, amount, currency, status, hold_until (только при успешной атрибуции на партнёра) |
reason | Причина, когда accepted: false (например POSTBACK_UNKNOWN_REF) |
accepted:false — это не ошибка ретрая
Ответ 200 с accepted: false (нет атрибуции или refund на несуществующую конверсию) — финальный. Это не сбой связи: значит, продажа органическая или клик не нашёлся. Не ретрайте такие ответы по кругу — обрабатывайте как штатный исход. Ретрай оправдан только на 5xx/таймаут.
Коды отклонения
| Код | HTTP | Когда | Реакция |
|---|---|---|---|
TENANT_API_KEY_INVALID | 401 | Ключ неизвестен, отозван или истёк | Проверьте X-Api-Key-Id/секрет, не ретрайте |
TENANT_API_KEY_SCOPE_DENIED | 403 | У ключа нет скоупа postback | Выпустите ключ со скоупом postback |
POSTBACK_BAD_SIGNATURE | 401 | HMAC-подпись не сошлась | Проверьте алгоритм подписи и что подписали те же байты |
POSTBACK_STALE_TIMESTAMP | 400 | X-Timestamp вне окна ±300 с | Синхронизируйте часы (NTP), пересоберите запрос |
POSTBACK_REPLAY | 409 | Повтор той же подписи в окне памяти | Сформируйте новый запрос с актуальным X-Timestamp |
POSTBACK_TENANT_MISMATCH | 403 | X-Tenant-Id не совпал с tenant ключа | Уберите/исправьте X-Tenant-Id |
POSTBACK_MISSING_FIELD | 422 | Нет обязательного поля для event_type | Добавьте недостающее поле |
POSTBACK_UNKNOWN_REF | 200 | Нет атрибутируемого клика (accepted:false) | Штатный исход — не ретрайте |
POSTBACK_REFUND_NO_CONVERSION | 200 | Refund на несуществующую конверсию (accepted:false) | Штатный исход — не ретрайте |
POSTBACK_RATE_LIMITED | 429 | Слишком много postback на ключ | Сбавьте темп, ретрай с backoff |
SERVICE_UNAVAILABLE | 503 | БД/хранилище недоступны | Ретрай с экспоненциальным backoff |
Полный формат конверта ошибки ({ "error": { "code", "message", "details" } }) — на странице Коды ошибок API.
Стратегия ретраев
Ретрайте только 5xx и сетевые таймауты — с экспоненциальным backoff. На любой 2xx (включая accepted:false) — стоп. На 4xx (кроме 409 POSTBACK_REPLAY, где нужно пересобрать подпись) — стоп и алерт: это, как правило, баг интеграции, а не временный сбой.
Полный пример
Конверсия-продажа с вычислением подписи. Базовый URL — {API_BASE} (точный домен даёт оператор).
API_BASE="https://api.meerpartners.com"
KEY_ID="key_3f9a1c"
SECRET="sk_live_9f2a…"
BODY='{"external_order_id":"ORD-558123","event_type":"sale","ref":"clk_5f2a9e","amount":"19.99","currency":"USD","status":"approved"}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIGNING=$(printf '%s\n%s\n%s\n%s' "POST" "/api/v1/postback" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -sS -X POST "$API_BASE/api/v1/postback" \
-H "Content-Type: application/json" \
-H "X-Api-Key-Id: $KEY_ID" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
--data "$BODY"Проверьте без боя
Перед подключением живого потока прогоните тестовый постбэк из кабинета (POST /api/v1/merchant/integration/test-postback) — он проходит через реальный флоу, но безопасен для проверки связки ключ ↔ оффер.