Security

Авторизация

API-ключи sk_test / sk_live, формат запросов, HMAC-подпись и идемпотентность. Bearer-ключ обязателен, подпись — опционально, но настоятельно.

Все защищённые запросы требуют заголовок Authorization: Bearer <api_key>. Ключи выпускаются на магазин в разделе Интеграция.

sk_test_…
тестовый ключ. Ходит в симулятор, реальный банк не вызывается. Доступен всегда.
sk_live_…
боевой ключ. Вернёт 403, если live-режим у мерчанта ещё не активирован.
Полный секрет показывается один раз при создании. Если потеряли, отзовите старый и выпустите новый.

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

Тела запросов и ответов — JSON в UTF-8. Дата и время в формате RFC 3339, в UTC. Денежные суммы целые, в минорных единицах валюты: копейки для RUB, центы для USD. Идентификаторы — UUIDv4.

curl -X POST https://api.balancedpay.pro/api/v1/public/payments \
  -H 'Authorization: Bearer sk_test_x9k…' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 7f2a-pay-1042'

Настройки в ЛК и HMAC-подпись

Всё, что ниже, делается в личном кабинете: /cabinet/integration → карточка магазина. Через публичное API эти параметры менять нельзя: они для человека, не для сервера.

На уровне магазина

  • API-ключи. sk_test_… ходит в симулятор, sk_live_… в боевой банк. На магазин можно выпустить несколько ключей, любой отзывается одной кнопкой. Полный секрет показывается ровно один раз — сохраните сразу.
  • Allow-list IP. Список IPv4, IPv6, CIDR, с которых разрешены запросы по ключам этого магазина. Запрос с другого адреса отклонится с 403 ip_not_allowed. Пустой список — ограничения нет.
  • HMAC-секрет магазина (thm_…). Используется, чтобы подписывать ваши запросы к нашему API. Если на магазине включена опция «Требовать подпись», запросы без заголовка X-Freefin-Signature: sha256=<hex> вернут 401. Это второй фактор поверх Bearer-ключа: даже если ключ протёк через лог или прокси, без секрета подделать запрос нельзя.

Подпись запроса (опционально)

Когда на магазине включена опция «Требовать подпись», каждый запрос к API нужно подписать секретом магазина (thm_…) и положить в заголовок X-Freefin-Signature: sha256=<hex>. Алгоритм: HMAC-SHA256 от raw body запроса, то есть тех самых байт, что уйдут в сеть, до любых преобразований. Для GET с пустым телом подписывается пустая строка.

import crypto from 'node:crypto'

const body = JSON.stringify({
  amount: 150000, currency: 'RUB', method: 'sbp', order_id: 'ORDER-1042',
})
const sig = 'sha256=' + crypto
  .createHmac('sha256', process.env.FREEFIN_TERMINAL_SECRET) // thm_…
  .update(body)
  .digest('hex')

await fetch('https://api.balancedpay.pro/api/v1/public/payments', {
  method: 'POST',
  headers: {
    'Authorization':       `Bearer ${process.env.FREEFIN_API_KEY}`,
    'Content-Type':        'application/json',
    'Idempotency-Key':     '7f2a-pay-1042',
    'X-Freefin-Signature': sig,
  },
  body, // ВАЖНО: ровно тот же body, что подписали — JSON.stringify один раз
})
Подписывайте ровно те байты, что уходят в сеть. Самая частая ошибка: вы сериализовали JSON в строку, посчитали по ней подпись, а потом передали обратно объект — HTTP-клиент сериализовал его заново, с другим порядком ключей или пробелами, и подпись больше не сходится. Считайте подпись по готовому байтовому телу и его же отправляйте.

Webhook endpoints

На магазин можно завести несколько endpoint'ов. У каждого свой URL, свой набор событий и свой секрет (whs_…). Этим секретом мы подписываем webhook'и, которые шлём вам. Подробнее в разделе Webhooks.

Не путайте два HMAC-секрета. Секрет магазина (thm_…) подписывает запросы от вас к нам. Секрет webhook-endpoint'а (whs_…) подписывает webhook'и от нас к вам. Они независимы и ротируются отдельно.

Идемпотентность

Для POST, создающих ресурс (платёж, выплата), передавайте заголовок Idempotency-Key до 64 символов. При повторном запросе с тем же ключом мы вернём ранее созданный объект и не создадим дубль. Ключ уникален в пределах одного мерчанта; используйте свой order_id или UUID на стороне приложения.

Повтор с тем же ключом и тем же телом — 200 OK с уже созданным объектом и полем idempotent: true. Повтор с тем же ключом, но другим телом — 409 idempotent_conflict.