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

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

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

Обзор #

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

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

СобытиеКогдаКлюч сверки
app.openedпользователь впервые открыл приложение по вашей ссылкеTelegram id
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) одинаков везде.

app.opened #

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

user_refстабильный id пользователя
tg_idTelegram id (число), если пришёл из Telegram
tg_usernameTelegram username без @, может отсутствовать
sourceкод/инвайт из ссылки привязки
{ "event":"app.opened","partner_code":"biblioglobus",
  "occurred_at":"2026-06-16T09:00:00+00:00",
  "data":{ "user_ref":"1f3c…e9","tg_id":387115382,"tg_username":"tourist","source":"biblioglobus" } }

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
{ "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

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.

Сверка по email #

Финансовая сверка между вами и ТурКартой идёт по email. Он присутствует в registration, card.issued, topup.funded и card.refunded. Событие app.opened приходит раньше, чем пользователь дал email, поэтому несёт только Telegram-идентификаторы и user_ref — связывайте его с остальными по user_ref.

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

По вашей просьбе менеджер отправит тестовое событие — постбэк app.opened с data.test = true. Оно подписано тем же секретом и проходит весь обычный путь (включая повторы), но не относится к реальному пользователю — удобно проверить приём и валидацию подписи.

{ "event":"app.opened","partner_code":"biblioglobus",
  "occurred_at":"…","data":{ "test":true,"note":"Turkarta postback endpoint test" } }

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

Версии #

ВерсияИзменения
v1Первый контракт: конверт, HMAC-подпись, 5 событий, повторы.

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