POST на адрес вебхука.webhookUrl в запросе на создание платежа. Если поле не передано, используется глобальный адрес, который администратор задал при онбординге. Переданный webhookUrl имеет приоритет над глобальным.X-Event-Type.| Событие | Когда |
|---|---|
payment.created | Платёж создан и платформа попыталась подобрать реквизит. Причина в теле всегда created. |
payment.status.changed | Статус изменился или площадка ещё раз просит подтверждение. |
merchantTransactionId может прислать payment.created ещё раз по тому же id. Ориентируйтесь на id платежа и на заголовок X-Delivery-Id, а не на факт «письмо пришло впервые».POST.webhookUrl из запроса, а если его не было, глобальный адрес с онбординга. Платформа не дописывает свой путь.| Заголовок | Пример | Зачем |
|---|---|---|
Content-Type | application/json | Тело это JSON. |
X-Event-Type | payment.status.changed | Какое это событие. |
X-Delivery-Id | 3b1c... uuid | Номер этой попытки доставки. Повторы того же письма имеют тот же id. |
X-Signature | 64 шестнадцатеричных символа | Подпись тела. Есть, если у мерчанта задан секрет. |
200 до 299 значит «принял». Тело ответа платформа не разбирает.2xx. Иначе платформа решит, что вы письмо приняли, и больше его не пришлёт.X-Signature.401.X-Delivery-Id. Если такой id уже был, ответить 200 и ничего не менять.id и merchantTransactionId.200.200, потом своя очередь.X-Delivery-Id, то же тело и та же подпись. Новый статус это уже новое письмо с новым X-Delivery-Id.X-Delivery-Id хотя бы несколько дней.X-Event-Type и в значении reason.| Поле | Всегда | Смысл |
|---|---|---|
id | да | Id платежа, тот же, что в API. |
merchantTransactionId | да | Ваш номер заказа. |
type | да | in приём или out выплата. |
status | да | Текущий статус. Имена ниже. |
amount | да | Сумма заказа строкой. |
currency | да | Валюта, например RUB. |
paidAmount | нет | Сколько фактически пришло. Нет, пока оплаты не было. |
amountUsd | нет | Сумма в долларах, если она посчитана. |
rail | да | Кто проводит. Часто trader. Может быть пустой строкой, если реквизит ещё не найден. |
reason | да | Почему отправлено это письмо. |
occurredAt | да | Время в UTC, формат RFC3339 с наносекундами. |
confirmationKind | нет | Только когда просят файл: receipt, video или other. |
confirmationNote | нет | Текст просьбы, если ег о написали. |
merchantRate, реквизитов и ссылки на страницу. Курс и реквизиты смотрите в GET /v1/payments/{id}.paidAmount, amountUsd, confirmationKind и confirmationNote в JSON не попадают. Пустой rail приходит как "".| status | Когда так бывает |
|---|---|
process | Платёж ещё идёт. Так выглядит payment.created, если реквизит найден или выплата только поставлена в очередь. |
confirmation_requested | Нужен чек, видео или другой файл. Смотрите confirmationKind. |
paid | Пришла ровно сумма заказа. |
underpaid | Пришло меньше. Смотрите paidAmount. |
overpaid | Пришло больше. Смотрите paidAmount. |
expired | Время вышло. |
cancel | Платёж отменён. |
chargeback | Оплата оспорена уже после успеха. |
error | Провести не удалось. Текст причины в reason. |
confirmation_requested приходит этим словом. В ответе GET /v1/payments/{id} тот же статус сейчас может прийти строкой 9. Это одно состояние.reason не является закрытым списком на все случаи. Ниже причины, которые код ставит сам. Свою внутреннюю причину ошибки платформа тоже может положить в это поле, например текст сбоя удержания баланса.| reason | Событие | status обычно |
|---|---|---|
created | payment.created | process |
paid 1500 of 1500 | payment.status.changed | paid, underpaid или overpaid. Числа это факт и сумма заказа. |
expired | payment.status.changed | expired |
cancel | payment.status.changed | cancel |
confirmation_requested | payment.status.changed | confirmation_requested |
rematched | payment.status.changed | process. Реквизит подобрали заново. |
rerouted | payment.status.changed | process. Платёж ушёл на другой способ проведения. |
ledger hold failed: ... | payment.status.changed | error. Не хватило баланса или учёт не записал удержание. |
paid X of Y сравнивайте по началу paid , а не как фиксированную строку. Суммы внутри зависят от платежа.X-Event-Type: payment.created.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"process","amount":"1500","currency":"RUB","amountUsd":"16.04","rail":"trader","reason":"created","occurredAt":"2026-10-23T11:48:00.123456789Z"}rail пустой. Имеет смысл ещё раз спросить GET /v1/payments/{id} или повторить создание с тем же номером заказа.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"process","amount":"1500","currency":"RUB","rail":"","reason":"created","occurredAt":"2026-10-23T11:48:00.123456789Z"}X-Event-Type: payment.status.changed.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"paid","amount":"1500","currency":"RUB","paidAmount":"1500","amountUsd":"16.04","rail":"trader","reason":"paid 1500 of 1500","occurredAt":"2026-10-23T11:55:00.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"underpaid","amount":"1500","currency":"RUB","paidAmount":"1000","amountUsd":"16.04","rail":"trader","reason":"paid 1000 of 1500","occurredAt":"2026-10-23T11:55:00.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"overpaid","amount":"1500","currency":"RUB","paidAmount":"2000","amountUsd":"16.04","rail":"trader","reason":"paid 2000 of 1500","occurredAt":"2026-10-23T11:55:00.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"expired","amount":"1500","currency":"RUB","rail":"trader","reason":"expired","occurredAt":"2026-10-23T12:18:00.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"cancel","amount":"1500","currency":"RUB","rail":"trader","reason":"cancel","occurredAt":"2026-10-23T11:50:00.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"confirmation_requested","amount":"1500","currency":"RUB","rail":"trader","reason":"confirmation_requested","occurredAt":"2026-10-23T11:52:00.123456789Z","confirmationKind":"receipt","confirmationNote":"Пришлите скрин перевода"}confirmationKind может быть receipt, video или other. Файл загружается методом из раздела про подтверждение в integratsiya.md.confirmationKind и confirmationNote.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"process","amount":"1500","currency":"RUB","rail":"trader","reason":"rematched","occurredAt":"2026-10-23T11:49:00.123456789Z"}rematched заберите платёж через API ещё раз: requisiteDetails могли смениться. Клиенту нужно показать новые реквизиты.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"process","amount":"1500","currency":"RUB","rail":"matching","reason":"rerouted","occurredAt":"2026-10-23T11:49:30.123456789Z"}{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"payout-1001","type":"out","status":"error","amount":"1500","currency":"RUB","rail":"","reason":"ledger hold failed: insufficient funds","occurredAt":"2026-10-23T11:48:02.123456789Z"}error и начало причины.{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"chargeback","amount":"1500","currency":"RUB","paidAmount":"1500","rail":"trader","reason":"chargeback","occurredAt":"2026-10-24T09:00:00.123456789Z"}chargeback в этом примере показана как типичный текст. Если платформа пришлёт другую строку в reason, решение принимайте по status, не по точному совпадению причины.json.loads и до любой перекодировки.https://shop.example/hooks/solvr?tenant=acme даёт строку /hooks/solvr?tenant=acme.https://shop.example/hooks/solvr даёт строку /hooks/solvr.X-Signature постоянным по времени сравнением, не оператором == по короткому контуру.whs_test-secret{"id":"pay_1","status":"paid"}/hooks/solvr?tenant=acme8cf29a80860ab4b6abfad15d363670e98da7670c79757941de117ec61be4122dwhsec_example, путь /hooks/solvr, тело:{"id":"6f1c2a40-1b2e-4c3d-9a8b-0e7d6c5b4a39","merchantTransactionId":"order-1001","type":"in","status":"paid","amount":"1500","currency":"RUB","paidAmount":"1500","amountUsd":"16.04","rail":"trader","reason":"paid 1500 of 1500","occurredAt":"2026-10-23T11:48:00.123456789Z"}2398b8e7a3e9f2daaec2f1c4937ac7b9767c8468640e40e3ce4f389b246d9953.raw_body берите из сырого тела запроса. webhook_url это адрес, на который платформа отправила письмо: из запроса или глобальный. Подпись считается по пути именно этого адреса.rawBody должен быть Buffer, не уже разобранный объект.file_get_contents('php://input'), не через $_POST.https://... или не попал query.sha256=. Префикса нет, в заголовке только hex.paid.X-Signature не будет. Пока секрет не выдан, такие письма не считайте доказательством оплаты.merchantTransactionId. Если не нашли, найти по id. Если нет ни того ни другого, ответить 200 и записать письмо в журнал, чтобы не получать повторы.X-Delivery-Id уже обработан, ответить 200 и выйти.paid, underpaid, overpaid, expired, cancel, error, chargeback), не откатывать его назад из-за запоздалого payment.created или process.paid закрыть заказ как оплаченный.underpaid и overpaid записать paidAmount и не ставить полную оплату автоматически.confirmation_requested запросить у клиента файл и отправить его в API.rematched и rerouted заново прочитать реквизиты через API и обновить страницу оплаты.expired, cancel, error закрыть заказ как неоплаченный.chargeback снять успешную оплату.200.