Постбэки ТурКарты

Server-to-server уведомления о ключевых событиях ваших приведённых пользователей: открыл приложение, зарегистрировался, выпустил карту, пополнил, получил возврат.

Версия контракта: v1. Базовый формат и подпись стабильны; новые поля в data могут добавляться (см. Версии).

Обзор #

Постбэк — это HTTP POST с JSON-телом, который ТурКарта отправляет на ваш заранее согласованный URL каждый раз, когда привязанный к вам пользователь достигает значимого этапа. Каждое сообщение подписано HMAC-подписью на вашем секрете, поэтому вы можете доверять его источнику и содержимому.

Пользователь привязывается к вам по первому касанию (deep-link t.me/turkarta_bot?startapp=<ваш-код> или одноразовый инвайт). Привязка неизменна. Постбэки приходят только по таким пользователям.

СобытиеКогдаКлюч сверки
app.openedпользователь впервые открыл приложение по вашей ссылкеuser_ref / clickid
registrationпользователь завершил регистрацию (подтвердил email)email
card.issuedкарта выпущена и активированаemail
topup.fundedпополнение карты зачисленоemail
card.refundedвозврат за выпуск или блокировка картыemail

Как это работает #

Подключение #

Адрес эндпоинта (URL) задаёт менеджер ТурКарты — это защита: постбэки несут email пользователей, поэтому сменить куда они уходят можно только на нашей стороне. Чтобы подключиться, передайте менеджеру:

URLHTTPS-эндпоинт, принимающий POST (например https://partner.example/turkarta/postback)
Событиякакие из пяти событий вам нужны (по умолчанию — все)

Подписным секретом вы управляете сами в партнёрском кабинете: кнопка «Показать секрет» открывает текущий, «Перевыпустить» генерирует новый. Храните его как пароль; им проверяется подпись каждого постбэка. После перевыпуска сразу обновите секрет на своей стороне — подпись считается уже новым секретом.

Там же — кнопка «Тестовое событие» (отправляет пробный app.opened на ваш URL) и статус всех доставок.

Формат запроса #

Метод POST, Content-Type: application/json. Заголовки:

X-Turkarta-Eventтип события, например topup.funded
X-Turkarta-Deliveryуникальный id этой доставки (используйте для дедупликации повторов)
X-Turkarta-Signaturesha256=<hex> — HMAC-SHA256 тела на вашем секрете

Тело — единый конверт. Метаданные на верхнем уровне, полезная нагрузка события — в data:

{
  "event": "topup.funded",
  "partner_code": "biblioglobus",
  "occurred_at": "2026-06-16T11:18:27.561029+00:00",
  "data": {
    "user_ref": "1f3c…e9",
    "email": "tourist@example.com",
    "topup_ref": "9ab2…7c",
    "card_ref": "4d10…aa",
    "amount_rub": "15000.00",
    "amount_usd": "150.00",
    "funded_at": "2026-06-16T11:18:27+00:00"
  }
}
eventтип события (дублирует X-Turkarta-Event)
partner_codeваш код партнёра
occurred_atмомент события, ISO-8601 UTC
dataполя, зависящие от типа события (см. ниже)
user_ref — это наш внутренний стабильный идентификатор пользователя (не Telegram id, не PII). Один и тот же user_ref приходит во всех событиях этого пользователя — используйте его, чтобы связать события одной воронки. Денежные суммы приходят строками, чтобы исключить ошибки округления float.

Проверка подписи #

Подпись считается над точными байтами тела, которые вы получили — не пересериализуйте JSON перед проверкой. Сравнивайте в постоянном времени.

signature = "sha256=" + HMAC_SHA256(key = secret, message = raw_request_body)  // hex

Python

import hashlib, hmac

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header or "")

# FastAPI / Flask: возьмите СЫРОЕ тело (await request.body() / request.get_data()),
# а не разобранный JSON, иначе подпись не сойдётся.

Node.js

const crypto = require("crypto");

function verify(secret, rawBody, header) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(header || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: используйте express.raw({ type: "application/json" }), чтобы получить
// req.body как Buffer (сырые байты), затем JSON.parse уже после проверки подписи.
Важно: при повторной доставке (retry) тело и подпись идентичны. При ручной переотправке из кабинета меняется только X-Turkarta-Delivery. Дедупликацию стройте на X-Turkarta-Delivery и/или на бизнес-id из data (topup_ref, card_ref).

События #

Ниже — поля data для каждого типа. Верхний уровень конверта (event, partner_code, occurred_at) одинаков везде.

Поле email — условное. Оно заполняется только после того, как вы приняли оферту в кабинете и менеджер ТурКарты включил вам доступ к персональным данным. До этого email приходит как null — стройте связку на user_ref и order_id, они есть всегда.

app.opened #

Пользователь впервые открыл приложение по вашей ссылке (момент привязки). Email на этом шаге ещё неизвестен — он появится в registration.

user_refстабильный id пользователя — связывайте с ним все последующие события
sourceкод/инвайт из ссылки привязки
clickid, subsваши метки из ссылки, если были (см. Сквозной clickid)
Приватность: Telegram-идентификаторы (id, username) в постбэках не передаются никогда. Идентификация пользователя — только user_ref и ваши собственные метки.
{ "event":"app.opened","partner_code":"biblioglobus",
  "occurred_at":"2026-06-16T09:00:00+00:00",
  "data":{ "user_ref":"1f3c…e9","source":"biblioglobus","clickid":"abc123xyz" } }

registration #

Пользователь завершил регистрацию и подтвердил email. Это якорь для сверки — именно по email мы сверяемся с партнёрами.

user_refстабильный id пользователя
emailподтверждённый email пользователя
registered_atмомент подтверждения, ISO-8601 UTC
{ "event":"registration","partner_code":"biblioglobus",
  "occurred_at":"2026-06-16T09:05:00+00:00",
  "data":{ "user_ref":"1f3c…e9","email":"tourist@example.com","registered_at":"2026-06-16T09:05:00+00:00" } }

card.issued #

Карта выпущена и активирована (выпуск оплачен и профинансирован).

user_refстабильный id пользователя
emailemail пользователя
card_refстабильный id карты в ТурКарте
masked_numberмаскированный номер, например •••• 1234
productкод продукта карты
issued_atмомент выпуска, ISO-8601 UTC
order_idнаш id заказа (цепочки платежа) — ключ сверки
order_uuidid платежа на стороне платёжной системы
{ "event":"card.issued","partner_code":"biblioglobus",
  "occurred_at":"2026-06-16T10:00:00+00:00",
  "data":{ "user_ref":"1f3c…e9","email":"tourist@example.com","card_ref":"4d10…aa",
           "masked_number":"•••• 1234","product":"visa_universe","issued_at":"2026-06-16T10:00:00+00:00" } }

topup.funded #

Пополнение карты зачислено. Суммы — строки; amount_usd — сумма, легшая на карту.

user_refстабильный id пользователя
emailemail пользователя
topup_refстабильный id пополнения (для дедупликации)
card_refid карты, которую пополнили
amount_rubоплачено пользователем, ₽ (строка)
amount_usdзачислено на карту, $ (строка)
funded_atмомент зачисления, ISO-8601 UTC
order_idнаш id заказа (цепочки платежа) — ключ сверки
order_uuidid платежа на стороне платёжной системы

card.refunded #

Возврат либо блокировка карты. Поле reason различает сценарии, amount_* = 0, если деньги не возвращались.

user_refстабильный id пользователя
emailemail пользователя
card_refid карты
reasonissuance_refund — возврат стоимости выпуска (карта не использовалась) · fraud_suspend — блокировка за мошенничество/злоупотребление
amount_rubвозвращено, ₽ (строка; 0.00 если ничего)
amount_usdвозвращено, $ (строка; 0.00 если ничего)
refunded_atмомент, ISO-8601 UTC
{ "event":"card.refunded","partner_code":"biblioglobus",
  "occurred_at":"2026-06-16T12:00:00+00:00",
  "data":{ "user_ref":"1f3c…e9","email":"tourist@example.com","card_ref":"4d10…aa",
           "reason":"issuance_refund","amount_rub":"14990.00","amount_usd":"0.00",
           "refunded_at":"2026-06-16T12:00:00+00:00" } }

Повторы и идемпотентность #

Доставка считается успешной при ответе 2xx. Любой другой код, таймаут или сетевая ошибка → повтор с экспоненциальной задержкой.

Попытокдо 8
Задержкаэкспоненциальная: ~30s, 1m, 2m, 4m … (потолок 1 час)
Таймаут10 секунд на ответ
После 8 неудачдоставка помечается exhausted; менеджер может переотправить вручную

Так как доставка at-least-once, один и тот же постбэк может прийти дважды. Делайте обработчик идемпотентным: запоминайте X-Turkarta-Delivery (или бизнес-id из data) и игнорируйте повтор.

Как отвечать #

Рекомендуемый порядок: (1) прочитать сырое тело, (2) проверить подпись, (3) проверить дедуп по X-Turkarta-Delivery, (4) поставить в свою очередь / записать, (5) вернуть 200.

Сверка #

Базовые ключи сверки — user_ref (стабильный id пользователя, есть в каждом событии) и order_id (id заказа в card.issued/topup.funded). Они не являются персональными данными и приходят всегда.

Сверка по email доступна после принятия оферты и включения доступа к персональным данным (см. примечание выше) — тогда email появляется в registration, card.issued, topup.funded и card.refunded.

Сквозной clickid для вашего трекера #

Если вы льёте трафик через собственный трекер (Keitaro, Binom и т.п.), передайте свой clickid и до пяти sub1…sub5 в ссылке — мы сохраняем их при первом заходе (first-touch, неизменны) и возвращаем в каждом постбэке в data.clickid / data.subs. Так ваш трекер атрибутирует конверсию по своему клику без сверки по email.

Web-ссылка — обычные query-параметры рядом с ?ref=:

https://app.turkarta.me/signup?ref=biblioglobus&clickid={clickid}&sub1={sub_id_1}

Telegram — один параметр startapp, поэтому clickid упаковывается после кода через двойное подчёркивание (sub1…5 в TG не передаются):

https://t.me/TurkartaBot/app?startapp=biblioglobus__{clickid}

В постбэке эти поля появляются внутри data (только если пользователь пришёл с ними — у органических пользователей их нет):

"data": {
    "user_ref": "1f3c…e9",
    "amount_usd": "150.00",
    "clickid": "abc123xyz",
    "subs": { "sub1": "fb", "sub2": "camp7" }
}
Настройте в своём трекере postback-URL так, чтобы он читал clickid из тела нашего события и сопоставлял его со своим кликом (макрос {subid}/{clickid} на вашей стороне).

Трекинговая ссылка — клики без своего трекера #

Нет своего трекера? Используйте нашу счётную ссылку — каждый переход засчитывается как клик (виден в вашем кабинете), после чего посетитель сразу попадает в приложение с вашей реферальной атрибуцией:

https://api.turkarta.me/go/biblioglobus

Варианты:

Воронка в кабинете: клики → приведённые пользователи → держатели карт → пополнения → объём. Считаются все переходы (не уникальные посетители); уникальные люди видны на следующем шаге воронки.

Тестирование #

Кнопка «Тестовое событие» в вашем кабинете (раздел «Обзор» → Постбэки) отправляет на ваш URL по одному демо-постбэку каждого из пяти типов — с реалистичными полями, включая clickid, subs, order_id/order_uuid и суммы. Все они подписаны вашим текущим секретом и проходят обычный путь доставки (включая повторы), так что можно отладить обработчик целиком, не дожидаясь реальных клиентов.

Каждый демо-постбэк несёт data.test = true — фильтруйте его от боевых событий по этому полю.

{ "event":"topup.funded","partner_code":"biblioglobus",
  "occurred_at":"…",
  "data":{ "test":true,"user_ref":"…","amount_rub":"10000","amount_usd":"120.10",
           "order_id":"…","order_uuid":"test-rail-payment-2",
           "clickid":"test-clickid-123","subs":{"sub1":"demo","sub2":"campaign7"} } }

Безопасность #

Версии #

ВерсияИзменения
v1Первый контракт: конверт, HMAC-подпись, 5 событий, повторы.
v1.1 · 2026-07-13Telegram-идентификаторы удалены из app.opened (приватность); email — только при включённом доступе к ПД; добавлены order_id/order_uuid; «Тестовое событие» шлёт все 5 типов с clickid/subs.

Мы можем добавлять новые поля в data и новые типы событий без смены версии — пишите обработчик так, чтобы незнакомые поля и типы игнорировались, а не ломали разбор.