Вебхуки

Уведомления через вебхуки

Откажитесь от опроса. Мы отправляем SMS код на ваш сервер сразу после получения, с подписью и повторными попытками.

Зачем вебхуки

Опрос тратит запросы и добавляет задержку. Вебхук доходит до вашего сервера в ту же секунду, когда приходит код.

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

Настройка

Зарегистрируйте один URL на аккаунт с помощью API ключа. Секрет подписи возвращается один раз.

curl -X POST https://smsbulk.net/api/users/webhook \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/smsbulk/webhook"}'

Ответ

{
  "id": "wep_2f8c1a9d",
  "url": "https://your-app.com/smsbulk/webhook",
  "sourceFilter": "API",
  "secret": "whsec_5f3a9c1e8b2d47a6903c5e1f7d8b4a26"
}

Сохраните секрет сразу

Открытый секрет показывается только в этом ответе. Последующие чтения возвращают маскированное значение. Если потеряли, обновите секрет в панели или через endpoint rotate.

Требования к URL

  • HTTPS на порту 443. Обычный HTTP и другие порты отклоняются.
  • Публичный адрес. Частные диапазоны, loopback и адреса облачных метаданных отклоняются при регистрации и повторно при каждой доставке.
  • Без редиректов. Ответ 3xx не отслеживается и считается неудачной попыткой.
МетодПутьНазначение
POST/api/users/webhookРегистрирует или заменяет endpoint, один раз возвращает секрет
GET/api/users/webhookЧитает текущий endpoint, секрет маскирован
PATCH/api/users/webhookМеняет URL или фильтр канала, включает отключённый endpoint
POST/api/users/webhook/rotateОбновляет секрет подписи с переходным окном
POST/api/users/webhook/testОтправляет тестовое событие без данных активации
DELETE/api/users/webhookУдаляет endpoint и останавливает все уведомления
GET/api/users/webhook/deliveriesПоследние 20 попыток доставки, только метаданные

События

Три события, все об одной активации.

СобытиеКогда отправляется
activation.code_receivedКогда приходит SMS код. На это событие реагирует большинство интеграций.
activation.cancelledКогда активация отменена. В теле указана причина и был ли возврат средств.
activation.expiredКогда время активации истекло без кода. В теле указан статус возврата.

Фильтр канала

По умолчанию уведомления создают только заказы через API. Укажите sourceFilter равным null, если нужны и заказы из панели.

Запрос и тело

Каждая доставка это POST с телом JSON. Поля выбираются по одному: внутренняя маршрутизация и детали себестоимости никогда не попадают в тело.

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

POST /smsbulk/webhook HTTP/1.1
Content-Type: application/json
User-Agent: SMSBulk-Webhook/1
X-SMSBulk-Signature: t=1785312000,v1=8f2a...c41d
X-SMSBulk-Event-Id: evt_del_9f2c1a
X-SMSBulk-Event-Type: activation.code_received
X-SMSBulk-Delivery-Attempt: 1

activation.code_received

{
  "id": "evt_del_9f2c1a",
  "type": "activation.code_received",
  "created": 1785312000,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "RECEIVED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": "483920",
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "sms_text": "Your code is 483920",
    "received_at": "2026-08-04T09:12:34.000Z"
  }
}

activation.cancelled и activation.expired

{
  "id": "evt_del_7b31de",
  "type": "activation.cancelled",
  "created": 1785312600,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "CANCELLED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": null,
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "reason": "user_cancel",
    "refunded": true,
    "refund_amount": "0.42"
  }
}

В будущих версиях могут появиться новые поля. Игнорируйте незнакомые поля вместо того, чтобы падать на них.

Проверка подписи

Каждый запрос несёт подпись HMAC SHA256, построенную из метки времени и сырого тела.

X-SMSBulk-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
signed_string = "<t>.<raw_request_body>"

Проверяйте сырые байты

Считайте подпись по точному телу, которое получили. Если разобрать JSON и собрать его заново, порядок ключей и пробелы изменятся, и подпись не совпадёт.

Значений v1 может быть несколько

Во время смены секрета заголовок несёт два значения v1. Собирайте их в список, а не в словарь: словарь оставит только последнее, и новый секрет тихо останется непроверенным.

Node.js

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const pairs = header.split(',').map((p) => {
    const i = p.indexOf('=');
    return [p.slice(0, i), p.slice(i + 1)];
  });

  const t = pairs.find(([k]) => k === 't')?.[1];
  // During a rotate window more than one signature arrives: collect them all.
  const signatures = pairs.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || signatures.length === 0) return false;

  // Replay window: reject anything older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return signatures.some(
    (s) =>
      s.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(s, 'hex'),
        Buffer.from(expected, 'hex'),
      ),
  );
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    pairs = [p.split("=", 1) for p in header.split(",")]

    t = next((v for k, v in pairs if k == "t"), None)
    # During a rotate window more than one signature arrives: collect them all.
    signatures = [v for k, v in pairs if k == "v1"]
    if t is None or not signatures:
        return False

    # Replay window: reject anything older than 5 minutes.
    if abs(time.time() - int(t)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return any(hmac.compare_digest(s, expected) for s in signatures)

Метка времени входит в подписываемую строку, поэтому можно отклонять уведомления старше нескольких минут. Сравнивайте подписи за постоянное время.

Ответ и повторы

Отвечайте быстро. Мы ждём ответ 10 секунд.

Делайте

  • Возвращайте 2xx сразу после сохранения события.
  • Ставьте тяжёлую работу в очередь и отвечайте первым делом.
  • Считайте свой 4xx ошибкой: он останавливает повторы.

Не делайте

  • Не запускайте долгие задачи до ответа, запрос истекает через 10 секунд.
  • Не отвечайте 3xx, редиректы не отслеживаются.
  • Не формируйте большое тело ответа, оно отбрасывается без чтения.
ПопыткаПауза передactivation.code_receivedcancelled и expired
1сразу
210s
360s
45m
515m
630m

Повторы происходят при сетевых ошибках, тайм-аутах, 408, 429 и 5xx. Любой другой 4xx сразу останавливает лестницу. code_received останавливается на 4 попытках, потому что активация завершается задолго до поздних шагов.

Дедупликация

Используйте X-SMSBulk-Event-Id как ключ идемпотентности. Повторы того же события приходят с тем же id.

Как минимум один раз, а не ровно один раз

На активацию и тип события создаётся одна запись, поэтому дубликаты редки. Но восстановление зависшей задачи может доставить ту же запись повторно. Сохраняйте id события и игнорируйте уже обработанный id.

Предохранитель

Мёртвый endpoint не вызывается бесконечно.

  • После 20 подряд неудачных доставок endpoint отключается.
  • Вы получаете письмо, а в панели видна причина отключения.
  • События, созданные при отключённом endpoint, не отправляются повторно. Догоняйте через GET /v1/activations.

Смена секрета

Смените секрет подписи без простоя.

  • Endpoint rotate один раз возвращает новый секрет.
  • В переходном окне отправляются и новая, и предыдущая подпись.
  • Переходите на новый секрет в своём темпе, после окна отправляется только новая подпись.

Настройте свой endpoint

Зарегистрируйте URL, отправьте тестовое событие и следите за списком доставок в панели.