API-ключи и HMAC-подпись
Как выпустить API-ключ MeerPartners, скоупы postback/sso/readonly, ротация и отзыв. Точный алгоритм HMAC-SHA256 подписи серверных запросов с примером вычисления.
API-ключ (tenant_api_key) — это пара «идентификатор + секрет» для серверных интеграций бизнеса. Идентификатор (key_id) передаётся открыто в заголовке, а секрет используется только для вычисления HMAC-подписи и никогда не отправляется в запросе.
Где выпустить ключ
Ключи выпускаются в кабинете бизнеса → раздел Интеграция. Управлять ими может только роль Владелец (merchant_owner).
/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 ключом без postback → TENANT_API_KEY_SCOPE_DENIED (403); то же для token-exchange без sso.
Минимальные привилегии
Выдавайте ключу только нужные скоупы и заводите отдельные ключи под разные задачи (например, один — только postback, другой — только sso). Компрометация узкого ключа наносит меньше вреда, а отзыв не ломает остальные интеграции.
Ротация и отзыв
/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