MMeerPartners docs

API-ключи и HMAC-подпись

Как выпустить API-ключ MeerPartners, скоупы postback/sso/readonly, ротация и отзыв. Точный алгоритм HMAC-SHA256 подписи серверных запросов с примером вычисления.

API-ключ (tenant_api_key) — это пара «идентификатор + секрет» для серверных интеграций бизнеса. Идентификатор (key_id) передаётся открыто в заголовке, а секрет используется только для вычисления HMAC-подписи и никогда не отправляется в запросе.

Где выпустить ключ

Ключи выпускаются в кабинете бизнеса → раздел Интеграция. Управлять ими может только роль Владелец (merchant_owner).

POST/api/v1/merchant/integration/keys🔒 Bearer JWT

Под капотом кабинет вызывает этот эндпоинт. При создании указываются имя и список скоупов:

Запрос на выпуск ключа
{ "name": "prod backend", "scopes": ["postback", "sso"] }
Ответ — секрет показывается ОДИН раз
{
  "id": 12,
  "key_id": "key_3f9a1c",
  "name": "prod backend",
  "scopes": ["postback", "sso"],
  "secret": "sk_live_9f2a…",
  "secret_tail": "…a1c",
  "warning": "Сохраните secret — он больше не будет показан",
  "created_at": "2026-06-14T00:00:00Z"
}

Секрет нельзя восстановить

Поле secret возвращается только в момент создания (и в явном «показать секрет» по запросу владельца). В базе хранится не сам секрет, а его зашифрованная форма. Если вы потеряли секрет — выпускайте новый ключ и отзывайте старый. Скопируйте секрет в своё хранилище секретов сразу.

Скоупы

Скоуп ограничивает, что ключ может делать. Допустимы три значения:

СкоупЧто разрешаетГде нужен
postbackОтправлять конверсииПостбэкPOST /api/v1/postback
ssoОбменивать ваш user-id на партнёрский токенToken-exchange
readonlyЗарезервирован для чтения данных через ключ

При выпуске нужно указать минимум один скоуп. Попытка отправить postback ключом без postbackTENANT_API_KEY_SCOPE_DENIED (403); то же для token-exchange без sso.

Минимальные привилегии

Выдавайте ключу только нужные скоупы и заводите отдельные ключи под разные задачи (например, один — только postback, другой — только sso). Компрометация узкого ключа наносит меньше вреда, а отзыв не ломает остальные интеграции.

Ротация и отзыв

POST/api/v1/merchant/integration/keys/{key_id}/revoke🔒 Bearer JWT

Отзыв немедленно делает ключ недействительным: любой последующий подписанный им запрос → TENANT_API_KEY_INVALID (401).

Ротация без даунтайма — это не отдельная кнопка, а процедура:

Выпустите новый ключ

Создайте второй ключ с теми же скоупами. Теперь активны оба.

Переключите бэкенд

Обновите конфиг своего сервера на новый key_id и секрет. Убедитесь, что postback проходит (используйте тестовый постбэк).

Отзовите старый ключ

Когда трафик полностью идёт через новый ключ — отзовите старый.

Список ключей (GET /api/v1/merchant/integration/keys) показывает только метаданные: key_id, имя, скоупы, статус, последние 4 символа секрета (secret_tail), время создания и последнего использования. Самих секретов в списке нет.

Алгоритм HMAC-подписи

Все серверные вызовы (postback и token-exchange) подписываются одинаково. Алгоритм идентичен для обоих эндпоинтов.

Заголовки запроса

X-Api-Key-Id: key_3f9a1c
X-Tenant-Id: 17               # опционально; если задан — должен совпасть с tenant ключа
X-Timestamp: 1717200000        # текущее время в epoch-секундах
X-Signature: 9b2f…c1           # hex(HMAC_SHA256(secret, signing_string))

Про X-Tenant-Id

Tenant всегда определяется по самому ключу в базе (анти-IDOR), поэтому X-Tenant-Id необязателен. Но если вы его передаёте, он должен точно совпадать с tenant, которому принадлежит ключ — иначе postback отклоняется с POSTBACK_TENANT_MISMATCH (403). Это удобная защита от случайной отправки данных не в тот бизнес.

Строка подписи

Подпись считается не от всего HTTP-запроса, а от канонической строки из четырёх частей, разделённых символом перевода строки \n:

{METHOD}\n{PATH}\n{X-Timestamp}\n{SHA256_hex(raw_body)}
ЧастьЧто подставить
{METHOD}HTTP-метод в верхнем регистре, например POST
{PATH}Путь запроса без хоста и query, например /api/v1/postback
{X-Timestamp}То же значение, что в заголовке X-Timestamp (epoch-секунды)
{SHA256_hex(raw_body)}SHA-256 от сырых байтов тела в hex (нижний регистр)

Затем:

X-Signature = hex( HMAC_SHA256( secret, signing_string ) )

Подписывайте те самые байты, что отправляете

Хэш считается по сырому телу запроса (raw_body). Если вы сериализуете JSON, посчитаете подпись, а потом библиотека переразберёт и пересоберёт тело (другие пробелы/порядок ключей) — байты изменятся и подпись не сойдётся. Сериализуйте тело один раз, считайте подпись от полученной строки и отправляйте ровно её. Для запросов без тела хэшируйте пустую строку.

Анти-replay

  • X-Timestamp должен быть в пределах ±300 секунд от времени сервера. Иначе для postback — POSTBACK_STALE_TIMESTAMP (400). Синхронизируйте часы сервера по NTP.
  • Каждая подпись одноразова в окне памяти (~600 с): повторная отправка той же подписи → POSTBACK_REPLAY (409). Не ретрайте запрос с той же подписью — формируйте новую с актуальным X-Timestamp.

Пример вычисления

Псевдокод и реальные реализации на двух языках. Тело здесь — компактный JSON без лишних пробелов; важно подписать ровно те байты, что уйдут в сеть.

body      = '{"external_order_id":"ORD-558123","event_type":"sale","ref":"…"}'
ts        = "1717200000"
method    = "POST"
path      = "/api/v1/postback"
 
body_hash = sha256_hex( bytes(body) )
signing   = method + "\n" + path + "\n" + ts + "\n" + body_hash
signature = hmac_sha256_hex( key = secret, msg = signing )
 
# Заголовки:
#   X-Api-Key-Id: <key_id>
#   X-Timestamp:  ts
#   X-Signature:  signature

Что дальше