С чего начать
«О, платёж!» отправляет события о движении денег по вашим счетам на указанный вами адрес. Дальше события обрабатывает ваша программа, например бот, учётная система или скрипт.
- Подключите банк в разделе «Банки». Сервис получит доступ к выписке по выбранным вами счетам.
- Создайте эндпоинт в разделе «Эндпоинты»: укажите адрес и отметьте счета. Сразу после создания на адрес отправляется пробное событие, по которому видно, доходят ли запросы.
- Сохраните секрет подписи на своей стороне и проверяйте подпись каждого события.
События приходят по мере того, как банк проводит операции. Сервис запрашивает выписку раз в пять минут, поэтому событие обычно приходит через несколько минут после проводки.
Все данные, которые сервис о вас хранит, можно выгрузить в профиле на вкладке «Мои данные». Выгрузка приходит одним файлом. Банковских токенов и секретов подписи в ней нет.
На той же вкладке есть кнопка «Закрыть профиль». Закрыть профиль самостоятельно можно, только если с ним не связаны данные организации; закрытие подтверждается ключом доступа (passkey). Если с профилем уже связаны данные организации (например, вы прошли шаг онбординга «Организация») или ключа доступа нет, вместо закрытия кабинет предложит «Направить запрос». Такой профиль закрывается по письму в поддержку. Договор организации и будущую оплату нужно прекратить отдельно.
Банки и согласия
Доступ подтверждается на стороне банка: вы входите в интерфейс банка и выдаёте разрешение там. Логин и пароль от банка сервис не получает и не хранит. Банк выдаёт сервису доступ только на чтение, распоряжаться деньгами сервис не может.
В окне подключения два экрана. Первый экран не относится к банку: на нём вы даёте согласие на передачу сведений, составляющих банковскую тайну. Без этого согласия банк не вправе передавать сведения сервису. Затем открываются вход в банк и выбор счетов.
Когда подключение завершено, окно закрывается автоматически, и кабинет продолжает настройку с того шага, на котором вы остановились.
На странице банка счета сгруппированы по юрлицам и подписаны номерами. Название счёта, полученное от банка, показано серым как подсказка: банки часто называют все счета одинаково. Если вы введёте своё название, оно будет показано вместо банковского.
Список счетов сервис запрашивает у банка отдельно от подключения, и банк может ответить не сразу. Пока список не получен, на шаге «Настройки счетов» показано «Запрашиваем счета у банка». Если банк отказал или не ответил за две минуты, появится кнопка «Запросить ещё раз»: по ней сервис заново спрашивает у банка список счетов. Если банк просит авторизоваться заново, вместо неё будет кнопка «Открыть страницу банка»: там есть «Авторизоваться повторно», после чего счета можно запросить ещё раз. Пустой список и отсутствие ответа от банка кабинет показывает по-разному. При первом подключении банка в онбординге кабинет так же ждёт счета и пишет «Банк подключён!», только когда они пришли.
Под списком счетов есть блок «Что мы видели по счёту» с последними операциями, полученными от банка. Он нужен, чтобы убедиться, что подключение работает и операции поступают в сервис. Выписку смотрите в банк-клиенте. В блоке также указано, с какой даты сервис хранит операции по счёту.
Счёт можно убрать из подключения, и события по нему перестанут приходить. При обновлении списка счетов убранный счёт не возвращается. Чтобы вернуть его, нажмите «Вернуть» рядом с надписью «Скрытых счетов: N» под списком.
Новый счёт, открытый в банке после подключения, появляется в кабинете автоматически: сервис обновляет список счетов каждого банка раз в сутки. Подключать банк заново не нужно.
Права, выданные банком, показаны в разделе «Согласия»: список прав, дата выдачи и срок действия. Рядом с названием права указан его код. Под этим кодом то же разрешение показано в интерфейсе банка.
| Право | Что даёт сервису |
|---|---|
Список счетов | Получить список ваших счетов, чтобы вы выбрали нужные |
Реквизиты счетов | Номер счёта и БИК. Передаются в событии при объёме «всё» |
Выписки по счетам | Операции по счёту. Без этого права событий не будет |
Подробности операций | Контрагент и назначение платежа |
Остатки по счетам | Остаток на счёте. Сейчас сервис его не показывает |
Набор прав определяет банк. У разных банков он разный, и некоторые банки выдают больше прав, чем нужно сервису. В разделе показаны все выданные права. Права, для которых в сервисе ещё нет названия, показаны кодом.
Отозвать доступ можно в разделе «Согласия» или в интерфейсе банка. При отключении в кабинете сервис сообщает об этом банку и прекращает свой доступ. События по этим счетам перестают приходить сразу.
Выписка готовится банком не сразу: сервис заказывает её, а когда она готова, забирает. Опрос идёт раз в пять минут, поэтому событие обычно приходит через несколько минут после проводки в банке.
У доступа к банку есть срок действия. Если доступ продлевается автоматически, вы этого не замечаете. Если продлить его нельзя, банк перестаёт отдавать выписку, и восстановить доступ можете только вы, пройдя авторизацию повторно.
За неделю до окончания доступа приходит предупреждение с датой. Если доступ потерян, подключение в кабинете выделяется красным и показывает дату, с которой события не приходят, а вам приходит уведомление. Если счета этого подключения отмечены в эндпоинте, на него также приходит событие connection.needs_reauthentication.
Эндпоинты
Эндпоинт — это адрес, на который сервис отправляет события, и список счетов, по которым они формируются. Эндпоинтов может быть несколько, и один счёт можно отметить в нескольких эндпоинтах.
Адрес должен использовать https и быть доступен из интернета. Адреса из частных сетей отклоняются при создании эндпоинта: сервис не отправляет на них запросы.
| Настройка | Что задаёт |
|---|---|
Адрес | Куда отправлять события. Адрес можно изменить, при этом ответственность за него подтверждается заново |
Счета | По каким счетам отправлять события. По неотмеченным счетам события на этот адрес не уходят |
Объём события | Все реквизиты или без номеров счетов и сведений о банках сторон |
Пауза | Пока эндпоинт на паузе, события на него не отправляются и в журнал доставки не записываются. После «Возобновить» пропущенные события не досылаются, их можно получить из истории операций. Эндпоинт и его настройки сохраняются |
Секрет подписи | Им подписывается каждое событие. Секрет можно показать и сменить |
Счета в списке сгруппированы по банкам и юрлицам и подписаны номерами, так как банки часто называют все счета одинаково. Отметка у банка или юрлица выбирает всю группу.
Только что подключённый банк появляется в списке сразу, даже если его счета ещё не получены. Банк передаёт их отдельно и может ответить не сразу. В этом случае в строке банка показано «Запрашиваем счета у банка…». Счета появятся в списке, как только придут, обычно через несколько секунд. Закрывать окно не нужно. Если банк не прислал счета, это будет указано в той же строке.
Если счёт больше не доступен в банке, например его закрыли или отозвали доступ, подписка эндпоинта на этот счёт перестаёт работать. Такой счёт помечается на странице эндпоинта.
Удаление эндпоинта сразу останавливает доставку. Журнал уже отправленных событий сохраняется.
По умолчанию на эндпоинт отправляются все события по отмеченным счетам. Чтобы отбирать события, задайте правила в блоке «Какие события присылать» на странице эндпоинта.
- Событие подходит под правило, если выполнены все условия этого правила.
- Если правил несколько, событие отправляется, когда подходит хотя бы под одно из них.
- Условия задаются по направлению, сумме, валюте, назначению платежа, названию и ИНН контрагента и по счёту.
- Для ИНН, валюты и счёта есть операторы «один из» и «ни один из», значения перечисляются через запятую.
- Для назначения платежа и названия контрагента есть оператор «подходит под выражение»: значение задаётся регулярным выражением.
Правила можно проверить на истории операций без сохранения. Кнопка «Примерить к истории» под редактором показывает, сколько операций за выбранный период прошли бы отбор и какие именно.
На странице эндпоинта есть две кнопки пробной отправки. «Проба выдуманным платежом» проверяет доставку: подпись, маршрут и код ответа. Она работает сразу и не зависит от банка и истории операций. «Проба настоящей операцией» отправляет последнюю операцию по счетам эндпоинта и проверяет, как ваш обработчик разбирает реальные данные: названия контрагентов, назначения платежей и пустые поля.
Кнопка «Почему не приходят события» проверяет по порядку: действует ли подписка, есть ли доступ к банку, отмечен ли счёт в эндпоинте, не остановлена ли доставка, были ли операции, что сделали правила отбора и чем закончилась доставка. Проверка останавливается на первом непройденном шаге. Этот шаг и есть причина: исправьте его, прежде чем проверять остальное.
Событие
Событие отправляется запросом POST с телом в формате JSON. Тело запроса (конверт) содержит четыре поля верхнего уровня: идентификатор, тип, время и данные.
| Поле | Что это |
|---|---|
id | Идентификатор события. При повторной доставке не меняется, по нему отбрасывают дубли |
type | Тип события. Типов четыре, они перечислены ниже |
timestamp | Время формирования события в UTC |
data | Данные события. Для payment.created это проводка |
Стороны платежа в data называются self (ваша сторона) и counterparty (контрагент) при любом направлении платежа. Направление указано в поле direction: credit означает поступление вам, debit означает списание с вашего счёта.
{
"id": "evt_16846918667cc913d752dd92cc1d35d958f862ca62a1cc06ab8decf79d0032af",
"type": "payment.created",
"timestamp": "2026-09-13T07:12:44Z",
"data": {
"direction": "credit",
"status": "Booked",
"amount": "73000.00000",
"currency": "RUB",
"bookedAt": "2026-09-13",
"valuedAt": null,
"documentNumber": "151",
"description": "Оплата по счёту № 12345 от 05.09.2026 за услуги. НДС не облагается.",
"self": {
"name": "ООО «Получатель»",
"inn": "0000000000",
"kpp": null,
"account": "40702810812500000000",
"bank": { "name": "Банк", "bic": "000000000" }
},
"counterparty": {
"name": "ООО «Плательщик»",
"inn": "0000000000",
"kpp": "000000000",
"account": "40702810400000000000",
"bank": { "name": "Банк плательщика", "bic": "000000000" }
},
"account": {
"id": "a6fb3ece15014c1da53690eb0ac9d4ae57b94e01",
"number": "40702810812500000000",
"bank": "Точка",
"alias": "Основной счёт"
}
}
}Объём «без номеров счетов» убирает из self и counterparty номер счёта и сведения о банке (название и БИК), а из account убирает номер счёта. Название, ИНН и КПП сторон, сумма и назначение платежа остаются.
{
"id": "evt_16846918667cc913d752dd92cc1d35d958f862ca62a1cc06ab8decf79d0032af",
"type": "payment.created",
"timestamp": "2026-09-13T07:12:44Z",
"data": {
"direction": "credit",
"status": "Booked",
"amount": "73000.00000",
"currency": "RUB",
"bookedAt": "2026-09-13",
"valuedAt": null,
"documentNumber": "151",
"description": "Оплата по счёту № 12345 от 05.09.2026 за услуги. НДС не облагается.",
"self": { "name": "ООО «Получатель»", "inn": "0000000000", "kpp": null },
"counterparty": { "name": "ООО «Плательщик»", "inn": "0000000000", "kpp": "000000000" },
"account": { "id": "a6fb3ece15014c1da53690eb0ac9d4ae57b94e01", "bank": "Точка", "alias": "Основной счёт" }
}
}Кроме событий о проводках, сервис отправляет три события о состоянии счёта и доступа к банку. Состав data у них свой, конверт и подпись такие же.
| type | Когда приходит |
|---|---|
payment.created | Проводка по счёту. Формат описан выше |
account.silent | По счёту нет поступлений заданное число дней. В data: account, silentDays, watchedDays, lastIncomeAt. Правило включается в настройках эндпоинта |
connection.needs_reauthentication | Банк перестал отдавать выписку, нужна повторная авторизация. В data: connection (id, bank), expiresAt и action: reconnect. События по всем счетам этого подключения прекратились |
connection.expiring | Доступ к банку скоро закончится. В data те же поля и daysLeft. Приходит заранее, пока выписка ещё поступает |
Те же данные можно получить запросами к API. События сервис отправляет сам, когда что-то происходит, а запрос вы отправляете, когда данные нужны вам. Если событие не дошло, операция остаётся в истории, и её можно получить запросом позже.
| Что нужно | Событием | Запросом |
|---|---|---|
Новая проводка | payment.created | GET /transactions, в MCP list_transactions |
Итоги за период | — | GET /transactions/summary, transactions_summary |
Контрагенты и обороты | — | GET /counterparties, list_counterparties |
Дошло ли событие | — | GET /webhooks/deliveries, list_deliveries |
Здоровье доставки | — | GET /webhooks/deliveries/summary, deliveries_health |
Жив ли доступ к банку | connection.needs_reauthentication, connection.expiring | GET /connectors, list_connections |
Подпись
Подпись соответствует спецификации Standard Webhooks. Для большинства языков есть готовые библиотеки проверки по этой спецификации, их можно использовать вместо собственного кода.
| Заголовок | Что в нём |
|---|---|
webhook-id | Идентификатор события. Совпадает с полем id в теле |
webhook-timestamp | Время подписи в секундах Unix |
webhook-signature | Префикс «v1,», затем HMAC-SHA256 от строки id.timestamp.body в base64 |
Подписывается строка из идентификатора, метки времени и тела, разделённых точками. Тело берётся байт в байт в том виде, в каком пришло. Если разобрать JSON и собрать его заново, строка изменится и подпись не совпадёт. Это самая частая ошибка при подключении.
Отклоняйте события, у которых метка времени отличается от текущего времени больше чем на пять минут. Это защищает от повторной отправки перехваченного запроса.
const crypto = require('crypto');
// Секрет со страницы эндпоинта начинается с whsec_.
// Ключ HMAC: часть после префикса, декодированная из base64.
const secret = Buffer.from(process.env.OPLATEZH_WEBHOOK_SECRET.slice(6), 'base64');
function isOurs(headers, rawBody) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
// Метка времени отличается от текущей больше чем на 5 минут: отклоняем
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
// rawBody: тело запроса без изменений. После JSON.parse и
// JSON.stringify строка будет другой, и подпись не совпадёт
const expected = 'v1,' + crypto
.createHmac('sha256', secret)
.update(`${id}.${timestamp}.${rawBody}`)
.digest('base64');
// По Standard Webhooks в заголовке может быть несколько подписей
// через пробел. Достаточно совпадения одной
return headers['webhook-signature'].split(' ').some(candidate =>
candidate.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)),
);
}<?php
// $rawBody: результат file_get_contents('php://input') без изменений
function isOurs(array $headers, string $rawBody): bool
{
$secret = base64_decode(substr(getenv('OPLATEZH_WEBHOOK_SECRET'), 6), true);
$id = $headers['webhook-id'];
$timestamp = (int)$headers['webhook-timestamp'];
if (abs(time() - $timestamp) > 300) {
return false;
}
$expected = 'v1,' . base64_encode(
hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $secret, true)
);
foreach (preg_split('/\s+/', trim($headers['webhook-signature'])) as $candidate) {
// hash_equals: время сравнения не зависит от того,
// на каком символе подписи расходятся
if (hash_equals($expected, $candidate)) {
return true;
}
}
return false;
}import base64, hashlib, hmac, os, time
SECRET = base64.b64decode(os.environ["OPLATEZH_WEBHOOK_SECRET"][6:])
def is_ours(headers, raw_body: bytes) -> bool:
event_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
signed = f"{event_id}.{timestamp}.".encode() + raw_body
expected = "v1," + base64.b64encode(
hmac.new(SECRET, signed, hashlib.sha256).digest()
).decode()
return any(
hmac.compare_digest(expected, candidate)
for candidate in headers["webhook-signature"].split()
)Доставка
Событие считается доставленным, если ваш сервер ответил любым кодом 2xx. Тело ответа не анализируется.
| Ответ вашего сервера | Действие сервиса |
|---|---|
Любой 2xx | Событие доставлено, повторов нет |
429 с заголовком Retry-After | Повтор через время, указанное в Retry-After |
429 без Retry-After | Повтор через минуту |
500 и другие коды 5xx | Повтор по расписанию ниже |
408 Request Timeout | Повтор по расписанию ниже |
Нет ответа, обрыв соединения, тайм-аут | Повтор по расписанию ниже |
400, 401, 403, 404 и другие коды | Событие отправляется один раз, повторов нет |
Паузы между попытками: 30 секунд, 2, 5, 15 и 30 минут, 1, 2, 4 и 8 часов. Всего десять попыток, от первой до последней проходит около шестнадцати часов. Заголовок Retry-After учитывается только в ответе 429: следующая попытка выполняется через указанное в нём время вместо паузы из расписания. Повторы относятся к событиям об операциях. События connection.* и account.silent отправляются один раз, без повторов.
Перенаправления не выполняются. Ответ 301 или 302 считается ошибкой доставки, поэтому указывайте конечный адрес.
Обрабатывайте события идемпотентно: сохраняйте id обработанных событий и пропускайте событие, если его id уже есть.
События отправляются только на публичные адреса. Частные диапазоны, адреса обратной петли (loopback) и адреса сервисов метаданных облаков отклоняются. Соединение устанавливается с IP-адресом, проверенным перед отправкой, поэтому доменное имя нельзя подменить между проверкой и запросом.
Пробное событие приходит с заголовком X-Test: true и действительной подписью. В остальном оно не отличается от обычного, и обработчик, который не проверяет этот заголовок, учтёт пробу как платёж. Проверяйте X-Test и выберите поведение: ответить 2xx без обработки или передать событие в тестовый контур.
- «Проба выдуманным платежом» проверяет доставку: подпись, маршрут и код ответа. Она работает сразу и не зависит от банка и истории операций.
- «Проба настоящей операцией» проверяет, как ваш код разбирает ваши же данные: контрагентов и назначения платежей. На названии с кавычками, пустом КПП у предпринимателя или назначении платежа на четыреста символов обработчик может сломаться при первом реальном платеже.
- «Проба настоящей операцией» отправляется с тем же идентификатором, что и настоящее событие по этой операции. Обработчик, который отбрасывает дубли по id, примет её за повтор.
Журнал событий
В разделе «Журнал доставки» кабинета показана каждая попытка доставки: время, адрес, ответ и его длительность. По клику на запись открываются тела запроса и ответа. Используйте журнал для отладки обработчика.
| Статус | Что случилось |
|---|---|
Доставлено | Ваш сервер ответил 2xx |
Ошибка | Сервер ответил кодом, при котором повтора не будет, или не удалась последняя попытка. Причина указана в записи |
Повтор | Сервер ответил 429, 408 или 5xx либо не ответил. Будет следующая попытка |
Не отправлено | Сервис не отправил запрос: частный адрес, недопустимая схема или имя не разрешается |
В карточке доставки к коду ответа есть пояснение. 401 означает, что подпись не совпала. Почти всегда причина в том, что подпись проверяется по заново собранному JSON вместо полученных байтов. 404 означает неверный путь. 500 означает, что ваш обработчик завершился с ошибкой на этом событии; в карточке видно, какие данные были отправлены. В каждом пояснении есть ссылка на нужный раздел документации.
Пробные отправки отмечены меткой «проба». Первая проба отправляется при создании эндпоинта. Пробы не учитываются при автоматической остановке адреса.
Готовые обвязки
В этом разделе приведён код, который вы запускаете на своей стороне. Сервис не размещает интеграции у себя: куда передаются ваши события и как они обрабатываются, решаете вы, и ключи от ваших систем остаются у вас.
Порядок подключения: разверните обработчик, укажите его адрес в эндпоинте, проверьте подпись. Если у вас нет своего сервера, используйте функцию для Yandex Cloud Functions ниже. Её можно развернуть за несколько минут, на малых объёмах она ничего не стоит.
Проверка подписи обязательна. Без неё любой, кто знает адрес обработчика, может отправить на него поддельное «поступление».
<?php
// Проверка подписи. Секрет со страницы эндпоинта, начинается с whsec_
$secret = getenv('OPLATEZH_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
// Метка времени отличается от текущей больше чем на 5 минут: отказ.
// Это защищает от повторной отправки перехваченного запроса
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit;
}
$key = base64_decode(substr($secret, strlen('whsec_')));
$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $key, true));
// hash_equals сравнивает строки за постоянное время
$ok = false;
foreach (preg_split('/\s+/', trim($received)) as $candidate) {
if ($candidate !== '' && hash_equals($expected, $candidate)) $ok = true;
}
if (!$ok) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Обработка события: $event['data']['amount'], $event['data']['counterparty']['name']
http_response_code(200);import base64, hashlib, hmac, os, time
SECRET = os.environ["OPLATEZH_WEBHOOK_SECRET"]
def verify(body: bytes, headers: dict) -> bool:
event_id = headers.get("webhook-id", "")
timestamp = headers.get("webhook-timestamp", "")
received = headers.get("webhook-signature", "")
if abs(time.time() - int(timestamp or 0)) > 300:
return False
key = base64.b64decode(SECRET.removeprefix("whsec_"))
signed = f"{event_id}.{timestamp}.".encode() + body
expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(hmac.compare_digest(expected, c) for c in received.split() if c)import crypto from 'node:crypto';
const SECRET = process.env.OPLATEZH_WEBHOOK_SECRET;
// Сравнение за постоянное время. timingSafeEqual бросает исключение
// при разной длине буферов, поэтому длина проверяется заранее: подпись
// другой длины должна дать 401, а не ошибку обработчика
function same(a, b) {
const left = Buffer.from(a);
const right = Buffer.from(b);
return left.length === right.length && crypto.timingSafeEqual(left, right);
}
export function verify(rawBody, headers) {
const id = headers['webhook-id'] ?? '';
const timestamp = headers['webhook-timestamp'] ?? '';
const received = headers['webhook-signature'] ?? '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(SECRET.replace('whsec_', ''), 'base64');
const expected =
'v1,' + crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
// rawBody: тело запроса без изменений, строка или Buffer. После разбора
// и повторной сборки JSON байты будут другими, и подпись не совпадёт
return received.split(/\s+/).some(c => c && same(expected, c));
}Отвечайте 2xx сразу после проверки подписи, а длительную обработку выполняйте через очередь. Если ответ не получен за время ожидания, доставка считается неудачной и повторяется.
Если сервера нет, используйте облачную функцию. Ниже обработчик для Yandex Cloud Functions: он проверяет подпись и возвращает ответ в формате, который ожидает площадка. Обработчик использует только встроенные модули, package.json не нужен.
const crypto = require('node:crypto');
const SECRET = process.env.OPLATEZH_WEBHOOK_SECRET;
// Регистр имён заголовков зависит от площадки, поэтому поиск
// идёт без учёта регистра
function header(headers, name) {
const key = Object.keys(headers || {}).find(k => k.toLowerCase() === name);
return key ? headers[key] : '';
}
function same(a, b) {
const left = Buffer.from(a);
const right = Buffer.from(b);
return left.length === right.length && crypto.timingSafeEqual(left, right);
}
module.exports.handler = async event => {
// Площадка может передать тело в base64 (isBase64Encoded). Подпись
// проверяется по исходным байтам тела, поэтому тело сначала декодируется
const raw = event.isBase64Encoded
? Buffer.from(event.body || '', 'base64')
: Buffer.from(event.body || '', 'utf8');
const id = header(event.headers, 'webhook-id');
const timestamp = header(event.headers, 'webhook-timestamp');
const received = header(event.headers, 'webhook-signature');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return { statusCode: 400, body: 'stale' };
}
const key = Buffer.from(String(SECRET).replace('whsec_', ''), 'base64');
const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), raw]);
const expected = 'v1,' + crypto.createHmac('sha256', key).update(signed).digest('base64');
if (!String(received).split(/\s+/).some(c => c && same(expected, c))) {
return { statusCode: 401, body: 'bad signature' };
}
const event_ = JSON.parse(raw.toString('utf8'));
// ─── обработка события ───
if (event_.type === 'payment.created' && event_.data.direction === 'credit') {
console.log('Поступило', event_.data.amount, 'от', event_.data.counterparty?.name);
}
// Ответ сразу. Длительную обработку передайте в очередь сообщений:
// тайм-аут ответа 15 секунд, а функция оплачивается за время работы
return { statusCode: 200, body: '' };
};- В консоли Yandex Cloud создайте функцию со средой выполнения Node.js 18 или новее.
- Загрузите код в файл index.js и укажите точку входа index.handler.
- В переменной окружения OPLATEZH_WEBHOOK_SECRET задайте секрет эндпоинта. Он начинается с whsec_.
- Включите «Сделать функцию публичной»: сервис отправляет события без авторизации, её заменяет подпись.
- Скопируйте ссылку для вызова вида https://functions.yandexcloud.net/d4e…
- При создании эндпоинта вставьте ссылку в поле «Адрес» и нажмите «Создать».
- После создания на адрес уходит пробное событие. Повторить пробу можно кнопкой «Проба выдуманным платежом» на странице эндпоинта. В логах функции видно, совпала ли подпись.
В Cloud.ru и на других площадках устройство такое же: запрос передаётся объектом с полями body, headers и признаком кодировки, а функция возвращает объект со statusCode. Названия полей могут отличаться.
Следующие примеры передают событие во внешние системы. Во всех примерах подпись уже проверена, а event содержит разобранный конверт.
// Подпись уже проверена (см. выше), event содержит разобранный конверт
const e = event.data;
const sign = e.direction === 'credit' ? 'Поступление' : 'Списание';
const text =
`${sign}: ${e.amount} ${e.currency}\n` +
`${e.direction === 'credit' ? 'От' : 'Кому'}: ${e.counterparty?.name ?? '—'}\n` +
`Назначение: ${e.description ?? '—'}`;
await fetch(`https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ chat_id: process.env.CHAT_ID, text }),
});// ВК Мессенджер: сообщение в беседу от имени сообщества.
// Нужен токен сообщества с правом «Сообщения». Версия API задаётся в v
const e = event.data;
await fetch('https://api.vk.com/method/messages.send', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
access_token: process.env.VK_TOKEN,
peer_id: process.env.VK_PEER_ID,
// random_id обязателен. Повторную отправку с тем же random_id
// ВК не доставит
random_id: event.id.slice(-9).replace(/\D/g, '') || String(Date.now() % 1e9),
message: `${e.direction === 'credit' ? 'Поступление' : 'Списание'}: ${e.amount} ${e.currency}\n` +
`${e.counterparty?.name ?? '—'}`,
v: '5.199',
}),
});// Входящий вебхук Битрикс24: комментарий в ленте сделки о поступлении
const e = event.data;
if (e.direction !== 'credit') return;
await fetch(`${process.env.B24_WEBHOOK}/crm.timeline.comment.add`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
fields: {
ENTITY_ID: Number(process.env.B24_DEAL_ID),
ENTITY_TYPE: 'deal',
COMMENT: `Поступило ${e.amount} ${e.currency} от ${e.counterparty?.name}. ` +
`Назначение: ${e.description ?? '—'}`,
},
}),
});// amoCRM: поиск компании по ИНН и примечание о платеже.
// Нужны долгосрочный токен интеграции и поддомен аккаунта
const e = event.data;
if (e.direction !== 'credit' || !e.counterparty?.inn) return;
const amo = async (path, init = {}) =>
(await fetch(`https://${process.env.AMO_SUBDOMAIN}.amocrm.ru/api/v4${path}`, {
...init,
headers: {
authorization: `Bearer ${process.env.AMO_TOKEN}`,
'content-type': 'application/json',
...(init.headers || {}),
},
})).json();
// Компания найдётся, если ИНН есть в её карточке:
// в отдельном поле или в названии
const found = await amo(`/companies?query=${encodeURIComponent(e.counterparty.inn)}`);
const company = found?._embedded?.companies?.[0];
if (!company) return;
await amo(`/companies/${company.id}/notes`, {
method: 'POST',
body: JSON.stringify([
{
note_type: 'common',
params: { text: `Поступило ${e.amount} ${e.currency}. ${e.description ?? ''}` },
},
]),
});| Куда | Что нужно получить заранее |
|---|---|
Телеграм | Токен бота от @BotFather и chat_id чата для сообщений |
ВК Мессенджер | Токен сообщества с правом «Сообщения» и peer_id беседы |
МАХ | Токен бота. Его Bot API похож на телеграмный, отличаются адрес и названия полей |
Битрикс24 | Входящий вебхук с правом crm и идентификатор сущности |
amoCRM | Долгосрочный токен интеграции и поддомен аккаунта |
Своя база | Ничего дополнительно: записывайте данные из разобранного события напрямую |
Без программирования события можно обрабатывать в конструкторах сценариев, например n8n или Make. Укажите в эндпоинте адрес вебхука конструктора и соберите сценарий в его редакторе. Подпись в конструкторе тоже нужно проверять, для этого используйте узел с кодом.
Если вы пользуетесь ассистентом, передайте ему спецификацию API и опишите задачу. Спецификация машиночитаемая и описывает в том числе события, которые сервис отправляет вам.
Вот спецификация API сервиса «О, платёж!»:
https://my.oplatezh.ru/api/openapi.yaml
Напиши обработчик вебхука на <язык/фреймворк>, который:
1) проверяет подпись по Standard Webhooks (заголовки webhook-id,
webhook-timestamp, webhook-signature; HMAC-SHA256 по "id.timestamp.body",
base64, с префиксом "v1,"; секрет с префиксом whsec_ — base64);
2) отклоняет отметку времени старше пяти минут;
3) на поступление (data.direction === "credit") делает <что нужно>;
4) отвечает 2xx сразу, тяжёлую работу выносит в очередь.
Подпись считается по сырым байтам тела: если площадка отдаёт его в base64
или фреймворк уже разобрал JSON — возьми исходные байты, иначе подпись
не сойдётся.Ассистента можно подключить к сервису напрямую. Тогда он сможет получать список ваших счетов и проверять, доходят ли события. Настройка описана в разделе «Ассистенты и MCP».
История операций
Сервис хранит операции по подключённым счетам: дату, сумму, направление, контрагента и назначение платежа. «О, платёж!» не ведёт учёт, история нужна для следующего:
- Правило отбора можно проверить на прошлых операциях и увидеть, какие из них оно пропустит
- Пробное событие можно отправить на основе настоящей операции
- Можно выяснить, почему платёж поступил, а событие не пришло
- Ассистент, подключённый по MCP, отвечает на вопросы о платежах без запроса к банку по каждому из них
Отдельного раздела с операциями в кабинете нет. Последние операции по счёту показаны на странице банка под списком счетов, чтобы можно было убедиться, что подключение работает и операции поступают в сервис. Отчётов, оборотов и итогов там нет: их смотрите в банк-клиенте или в бухгалтерии.
| Что | Запрос |
|---|---|
Лента операций | GET /api/transactions, в MCP list_transactions |
Итоги за период | GET /api/transactions/summary, transactions_summary |
Контрагенты с оборотом | GET /api/counterparties, list_counterparties |
Одна операция целиком | GET /api/transactions/{id} |
История сохраняется по всем подключённым счетам, в том числе по счетам, не отмеченным ни в одном эндпоинте. Счета, отмеченные в эндпоинтах, опрашиваются раз в пять минут, остальные — раз в час.
Банк передаёт о контрагенте только название и ИНН. Остальные сведения сервис получает по ИНН из государственного реестра: название, статус, адрес, вид деятельности и руководителя.
Для чтения истории через API ключу нужно право «Читать историю операций и контрагентов». Оно выдаётся отдельно от права на журнал доставок: журнал показывает, какие события сервис отправил, а история содержит сами операции. Ключ можно ограничить отдельными счетами, тогда операции по остальным счетам ему недоступны.
Ключи API
Ключ API нужен программе, которая обращается к сервису. Ключ передаётся в заголовке Authorization: Bearer и даёт только те права, которые вы выбрали при выдаче. Ключи выдаются в разделе «Ключи API» кабинета.
При выдаче выбирается срок действия: 30 дней, 90 дней, год или бессрочно. По умолчанию выбрано 90 дней. Ключ передаётся в сторонние скрипты и системы, и срок действия ограничивает ущерб, если ключ утечёт незаметно. После окончания срока запросы с этим ключом получают ответ 401. Новый ключ можно выдать в любой момент, действующий можно отозвать до окончания срока.
В списке ключей показан срок действия каждого ключа. Истёкший ключ помечен красным, за неделю до окончания срока появляется предупреждение. Бессрочный ключ помечен как бессрочный.
| Право | Что разрешает |
|---|---|
read:accounts | Видеть подключённые банки и счета |
write:accounts | Подключать и отключать банки, менять счета |
read:endpoints | Видеть эндпоинты и их настройки |
write:endpoints | Заводить и менять эндпоинты, читать и менять секреты подписи |
read:events | Читать журнал доставок |
read:transactions | Читать историю операций и контрагентов |
Ключ можно ограничить отдельными счетами. Права определяют, какие действия разрешены ключу, а список счетов определяет, к каким счетам эти действия применяются. Например, боту, который следит за одним расчётным счётом, не нужна выписка по остальным. Без ограничения ключ имеет доступ ко всем счетам, включая подключённые позже.
- Операции, доставки, контрагенты и счета, относящиеся к другим счетам, такому ключу не возвращаются.
- Запрос к другому счёту по идентификатору, в параметре optionId или в разборе счёта, возвращает 403. Пустой список в этом случае не возвращается, чтобы его не приняли за отсутствие операций.
- Такой ключ не может отметить в эндпоинте счёт, которого нет в его списке: это обошло бы ограничение.
- Такой ключ также не может убрать счёт из подключения, вернуть скрытые счета и отключить банк: эти действия затрагивают счета вне его списка.
Запрос, на который у ключа нет права, получает ответ 403. Например, ключ с правом только на чтение журнала доставок не может отключить банк или создать эндпоинт. Чтение истории операций и чтение журнала доставок разделены на два права, чтобы боту для контроля доставок не нужно было давать доступ к истории платежей.
Действия с учётной записью ключом не выполняются: оплата подписки, изменение профиля, выгрузка данных и удаление аккаунта. Они доступны только в кабинете.
Значение ключа показывается один раз, при выдаче. Сервис хранит только хеш ключа, поэтому показать значение повторно невозможно. Если ключ потерян, отзовите его и выдайте новый.
Выдавайте ключу только те права, которые нужны для его задачи.
В списке ключей показано время последнего использования каждого ключа. Ключ, который ни разу не использовался, можно отозвать без последствий для интеграций.
API
Через API доступны счета и подключение банков, эндпоинты и их правила, журнал доставок и история операций в пределах прав ключа (см. раздел «Ключи API»). Действия с учётной записью через API недоступны: профиль, выгрузка данных, оплата, принятие документов и управление ключами выполняются только в кабинете. Спецификация опубликована в формате OpenAPI 3.1 и доступна без авторизации.
Кроме методов API, спецификация описывает события, которые сервис отправляет вам (раздел webhooks). Файл можно передать инструменту генерации кода или языковой модели, чтобы получить код интеграции.
# Счета подключённых банков. Из них составляется список счетов эндпоинта
curl https://my.oplatezh.ru/api/accounts \
-H "Authorization: Bearer $OPLATEZH_API_KEY"
# Сводка доставки за сутки: доставлено, не доставлено, доля отказов
curl https://my.oplatezh.ru/api/webhooks/deliveries/summary \
-H "Authorization: Bearer $OPLATEZH_API_KEY"
# Последние доставки с ошибкой
curl "https://my.oplatezh.ru/api/webhooks/deliveries?status=failed&limit=10" \
-H "Authorization: Bearer $OPLATEZH_API_KEY"
# Отправить пробное событие
curl -X POST https://my.oplatezh.ru/api/endpoints/END_ID/test \
-H "Authorization: Bearer $OPLATEZH_API_KEY"При ошибке валидации возвращается код 422 и поле errors: ключ в нём соответствует имени поля запроса, значение содержит список ошибок по этому полю. Если у ключа недостаточно прав, возвращается 403. Если ключ недействителен, возвращается 401.
Счёт в эндпоинте задаётся парой connectorId и optionId. GET /accounts возвращает все ваши счета плоским списком в том формате, в котором они передаются в accounts[] эндпоинта.
Завершить подключение банка через API нельзя: согласие на доступ к счёту даёт владелец счёта в интерфейсе банка. Программа может начать подключение и дождаться результата.
# 1. Начать подключение. Ответ содержит идентификатор попытки
curl -X POST https://my.oplatezh.ru/api/drivers/tochka/state \
-H "Authorization: Bearer $OPLATEZH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"action":"authorize"}'
# 2. Владелец счёта проходит авторизацию в банке
# 3. Запросить результат попытки
curl https://my.oplatezh.ru/api/connections/ID_ПОПЫТКИ \
-H "Authorization: Bearer $OPLATEZH_API_KEY"Попытка подключения действует один час. Поле status принимает значения: pending, если авторизация в банке ещё не завершена; completed, если подключение создано (в ответе есть connectorId); failed, если подключение не удалось (причина в поле error).
Ассистенты и MCP
Ассистента, например Claude, помощника в редакторе кода или собственного бота, можно подключить к сервису напрямую. Подключённый ассистент может получить список счетов, проверить, дошли ли события, и найти нужный платёж.
Подключение работает по протоколу MCP (Model Context Protocol), стандартному способу подключения ассистентов к внешним сервисам. Писать код не нужно: в настройках ассистента укажите адрес сервера и ключ API.
{
"mcpServers": {
"oplatezh": {
"type": "http",
"url": "https://my.oplatezh.ru/api/mcp",
"headers": {
"Authorization": "Bearer ВАШ_КЛЮЧ"
}
}
}
}Адрес сервера MCP: https://my.oplatezh.ru/api/mcp. Для подключения нужен обычный ключ API, тот же, что для прямых запросов. Он выдаётся в разделе «Ключи API» кабинета.
Возможности ассистента в зависимости от прав ключа:
| Действие | Право ключа |
|---|---|
Показать счета подключённых банков, узнать результат подключения банка | read:accounts |
Начать подключение банка | write:accounts |
Показать эндпоинты и их настройки | read:endpoints |
Создать или изменить эндпоинт, отправить пробное событие | write:endpoints |
Показать журнал доставок и долю ошибок | read:events |
Найти платежи, посчитать итоги, показать контрагентов | read:transactions |
Завершить подключение банка ассистент не может при любых правах ключа: согласие на доступ к счёту вы даёте сами в интерфейсе банка. Это требование банка. Ассистент может начать подключение и дать вам ссылку, авторизацию в банке проходите вы.
Ассистенту также недоступны действия с учётной записью: оплата подписки, изменение профиля, выгрузка данных, удаление аккаунта и выпуск нового ключа. Они доступны только в кабинете.
Проверить подключение можно без ассистента. Этот запрос возвращает список инструментов, доступных ключу:
curl https://my.oplatezh.ru/api/mcp \
-H "Authorization: Bearer $OPLATEZH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Инструмент list_connections показывает состояние доступа к каждому банку: с какой даты банк не отдаёт выписку и нужна ли повторная авторизация. С него стоит начинать, если события перестали приходить: отсутствие поступлений и потерянный доступ выглядят одинаково, но требуют разных действий.
Инструмент diagnose_account выясняет, почему по счёту не приходят события. Он проверяет по порядку, действует ли подписка, есть ли доступ к банку, отмечен ли счёт в эндпоинте, что сделали правила отбора и чем закончилась доставка, и останавливается на первой непройденной проверке. Инструмент preview_filters применяет правила отбора к истории операций без сохранения и показывает, сколько операций прошли бы отбор и какие именно.
Ограничение частоты общее с остальным API: 60 запросов в минуту. Обращение ассистента к серверу MCP считается одним запросом, хотя внутри оно вызывает метод API.