Assalam Docs

Вебхуки

Узнавать о событиях, не опрашивая нас: подпись, повторы, типы событий

Часть событий происходит не в тот момент, когда вы делаете запрос. Банк подтверждает списание отдельным шагом, клиент вводит код из СМС когда захочет, а покупатель уходит на страницу оплаты и возвращается не сразу. Чтобы узнать об этом, есть два пути: спрашивать нас периодически или получить уведомление. Второй и есть вебхук.

Что приходит на ваш адрес
{
  "id": "cmsr9f2c",
  "object": "event",
  "type": "payment_intent.succeeded",
  "livemode": false,
  "created": "2026-08-19T13:40:12.004Z",
  "data": {
    "id": "cmsr1a2b",
    "object": "payment_intent",
    "amount": 5000000,
    "status": "succeeded"
  }
}
Заголовки доставки
Assalam-Signature: t=1771582439,v1=8f3c…
Assalam-Event-Id: cmsr9f2c
Assalam-Event-Type: payment_intent.succeeded
Assalam-Delivery-Id: cmsr4e7a
Content-Type: application/json

Как подключить

Адрес добавляется в кабинете, в разделе «Вебхуки». Там же выдаётся секрет подписи — он начинается с whsec_. Адрес должен начинаться с https:// и быть доступен из интернета.

Тестовый и боевой режимы разделены: события тестового режима приходят только на тестовые адреса. Так репетиция не заденет боевую обработку.

Подпись

Каждая доставка подписана заголовком Assalam-Signature. В нём метка времени и код: t=1771582439,v1=8f3c…. Подписывается строка «метка.тело» алгоритмом HMAC-SHA256 на вашем секрете.

Метка времени внутри подписи нужна не для красоты: без неё перехваченную доставку можно отправить вам повторно спустя сутки, и подпись останется верной. Отвергайте доставки старше пяти минут.

Ваш сервер · Проверка подписи
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=')),
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) return false;           // старую доставку не принимаем

  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest();

  const received = Buffer.from(parts.v1, 'hex');
  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(expected, received)
  );
}
Ваш сервер · Обработчик
app.post('/assalam/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verify(
      req.body.toString('utf8'),        // именно СЫРОЕ тело
      req.get('Assalam-Signature'),
      process.env.ASSALAM_WEBHOOK_SECRET,
    );
    if (!ok) return res.sendStatus(400);

    res.sendStatus(200);                // отвечаем сразу
    handleAsync(JSON.parse(req.body));  // работаем после
  });

Подписывается сырое тело

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

Доставка и повторы

Успехом считается любой ответ с кодом 2xx. Всё остальное — включая таймаут и отсутствие ответа — считается неудачей, и мы пробуем снова с растущими промежутками, примерно трое суток.

Отвечайте быстро, до обработки. Если делать работу до ответа, наш таймаут в 10 секунд превратит успешно обработанное событие в неудачную доставку, и оно придёт снова.

Если доставка не удалась
попытка 1  сразу
попытка 2  через 1 минуту
попытка 3  через 5 минут
попытка 4  через 30 минут
попытка 5  через 2 часа
попытка 6  через 6 часов
попытка 7  через 12 часов
попытка 8  через сутки
попытка 9  через сутки

дальше доставка помечается
неудавшейся

Одно событие может прийти дважды

Так устроены все системы доставки: мы не можем отличить «ваш ответ потерялся» от «вы не ответили». Запоминайте id обработанных событий и пропускайте повторы.

Если ваш сервис лежал дольше, чем длятся повторы, историю можно перечитать через GET /v1/events — там те же события с постраничным обходом.

Ваш сервер · Догнать пропущенное
curl "https://api.assalam.uz/v1/events?limit=20" \
  -H "Authorization: Bearer sk_test_..."

# следующая страница
curl "https://api.assalam.uz/v1/events?limit=20&starting_after=cmsr9f2c" \
  -H "Authorization: Bearer sk_test_..."

Какие события бывают

Имя строится как объект.что_произошло и всегда в прошедшем времени: событие описывает случившееся, а не намерение.

Ниже — то, что платформа отправляет на самом деле. Какие вам нужны, зависит от поверхности: продаёте товары — хватит трёх событий заказа, списываете с сохранённой карты — двух событий платежа.

СобытиеКогда приходит
order.paidпокупатель заплатил на нашей странице
order.failedбанк отказал в оплате заказа
order.expiredпокупатель не заплатил до истечения срока
payment_intent.succeededсписание с привязанной карты прошло
payment_intent.failedсписание не прошло, причина — в failureCode

При подключении адреса можно выбрать нужные типы. Если не выбирать — приходят все. Новые типы мы добавляем со временем, поэтому не считайте неизвестный тип ошибкой: просто пропускайте его.

В списке подписки типов больше, чем в таблице

Платформа принимает подписку и на типы, которых сегодня никто не отправляет (payment_intent.canceled, refund.succeeded, события карт и сессий) — они заведены заранее. Подписаться на такой тип можно, дождаться его нельзя: стройте логику только на событиях из таблицы.

Что внутри data

Тот же объект, что вернул бы соответствующий метод API, на момент события. Это снимок: если платёж потом изменится, снимок останется прежним. За текущим состоянием обращайтесь к API по идентификатору.

Дальше по теме

На этой странице