Incognum

Разработчикам

Автоматизация SMS
с помощью вебхуков за десять минут.

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

  • 9 мин чтения
  • Обновлено 21.05.2026
  • Доступно без регистрации
Зелёный пакет летит между стеклянной панелью и серверной стойкой, оставляя более бледные повторные дуги

Перед началом

Три вещи и пять минут.

  1. Учётная запись и баланс. Создайте ключ доступа и пополните баланс криптовалютой. Заказ оплачивается с баланса, поэтому на нём должны быть средства.
  2. API-ключ с областью действия admin. Заказ номеров расходует деньги, чего ключи read и write делать не могут. Создайте его в разделе API-ключи; ключ показывается один раз.
  3. Доступный нам конечный адрес HTTPS. Обычный HTTP отклоняется. Для локальной разработки подойдёт любой инструмент туннелирования.

Не храните ключ в репозитории. Ключ admin позволяет заказывать номера, пока баланс не опустеет. Используйте переменную окружения, хранилище секретов — что угодно, кроме файла в репозитории. Если ключ утёк, прежде всего отзовите его в панели.

Шаг 1 — заказать линию

Потребуется один запрос. Он списывает средства с баланса и завершается после подтверждения выделения номера; линия начинает принимать сообщения примерно через минуту.

POST /v1/numbers
curl -X POST https://api.incognum.com/v1/numbers \
  -H "Authorization: Bearer $NKP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"country":"de","type":"mobile","period":1,"label":"signup-bot"}'

# 201 Created
{
  "id": "num_8f2c1a94",
  "e164": "+4915735550088",
  "status": "active",
  "renews_at": "2026-10-21T09:14:02Z",
  "charged": { "monthly": 12.90, "activation": 10.00, "total": 22.90 }
}

Всегда передавайте Idempotency-Key. На вашей стороне тайм-аут неотличим от сбоя. С этим заголовком повторный запрос вернёт сохранённый ответ, а не закажет второй номер; ключ хранится 24 часа.

Шаг 2 — зарегистрировать вебхук

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

POST /v1/webhooks
curl -X POST https://api.incognum.com/v1/webhooks \
  -H "Authorization: Bearer $NKP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.org/hooks/nkp","events":["message.received"]}'

# 201 Created
{
  "id": "whk_77c1e0",
  "url": "https://example.org/hooks/nkp",
  "events": ["message.received"],
  "secret": "whsec_2b91f4c7a0d3…"
}

Подписывайтесь только на те события, которые обрабатываете. Для этой задачи полезны следующие:

  • message.received — получено SMS, код уже извлечён.
  • number.active — заказанная линия активирована.
  • number.grace — продление не удалось из-за недостатка средств. Настройте оповещение: это предупреждение за 3 дней до исчезновения линии.
  • topup.credited — пополнение подтверждено.

Шаг 3 — проверить подпись

Конечный адрес, принимающий любые отправленные ему данные, — не вебхук, а публичный API записи. Проверяйте подпись, прежде чем доверять телу запроса.

Подпись представляет собой HMAC-SHA256 от timestamp + "." + raw_body, где ключом служит секрет конечного адреса. Результат кодируется в шестнадцатеричном виде и передаётся в заголовке X-NKP-Signature как v1=….

handler.py
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["NKP_WEBHOOK_SECRET"].encode()
seen = set()   # в рабочей среде: Redis с истечением срока хранения

@app.post("/hooks/nkp")
def hook():
    raw = request.get_data()                      # ИСХОДНОЕ тело без повторной сериализации
    ts  = request.headers.get("X-NKP-Timestamp", "")
    sig = request.headers.get("X-NKP-Signature", "").split("=", 1)[-1]

    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(400)                                 # слишком старый запрос: повтор
    mine = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(mine, sig):
        abort(401)

    delivery = request.headers["X-NKP-Delivery"]
    if delivery in seen:
        return "", 200                             # уже обработано, но всё равно подтверждаем
    seen.add(delivery)

    event = request.get_json()
    if event["event"] == "message.received":
        enqueue(event["data"])                     # обрабатываем ПОСЛЕ ответа
    return "", 200

В этом обработчике важны три вещи; если пропустить любую из них, можно получить реальный сбой:

  • Подписывайте исходное тело. Разбор и повторная сериализация JSON меняют байты и делают HMAC недействительным.
  • Сравнивайте за постоянное время. Оператор == при сравнении подписей допускает утечку по времени выполнения.
  • Отклоняйте старые метки времени. Без пятиминутного окна перехваченный запрос можно воспроизводить бесконечно.

Что вы получите

POST на ваш конечный адрес
POST /hooks/nkp HTTP/1.1
X-NKP-Event: message.received
X-NKP-Delivery: evt_6f0b28d4
X-NKP-Timestamp: 1789033541
X-NKP-Signature: v1=6b3a1f8e7c0d94aa5e2b…
Content-Type: application/json

{
  "event": "message.received",
  "created_at": "2026-09-21T09:45:41Z",
  "data": {
    "id": "msg_5b1f77c0",
    "number": "num_8f2c1a94",
    "e164": "+4915735550088",
    "from": "Telegram",
    "body": "Ваш код входа — 72194. Никому его не сообщайте.",
    "code": "72194",
    "received_at": "2026-09-21T09:45:41Z"
  }
}

Поле code извлекается на сервере по тем же правилам, что используются в панели: группы от четырёх до восьми цифр, пробелы и дефисы удаляются, собственный номер отправителя игнорируется. Если ничего похожего на код нет, значение будет null, а поле body останется доступно для самостоятельного разбора.

Повторные попытки, порядок и другие подводные камни

  • Доставка выполняется как минимум один раз. Удаляйте дубликаты по X-NKP-Delivery. Один ID никогда не обозначает два разных события.
  • Порядок не гарантируется. Два сообщения с разницей в секунду могут прийти в любой последовательности. Если порядок важен, сортируйте по created_at.
  • На ответ даётся десять секунд. Более медленный обработчик считается неудачным, и событие доставляется повторно. Сначала подтвердите приём, затем выполняйте работу.
  • Восемь попыток примерно за сутки с интервалами 10 с, 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч и 24 ч.
  • Три дня непрерывных сбоев отключают конечный адрес, о чём сообщит панель. Ничего не теряется: сообщения по-прежнему доступны через API.

Оставьте резервный опрос. Вебхуки обеспечивают скорость, но не являются источником истины. Недорогой запрос GET /v1/messages?since=… раз в несколько минут получит всё, что конечный адрес пропустил во время простоя, и ничего не стоит, когда получать нечего.

Шаг 4 — поддерживать работу

Автоматизация, которая заказывает номера и никогда их не освобождает, незаметно опустошает баланс. Заведите две привычки:

  • Освобождайте ненужные линии. Запрос DELETE /v1/numbers/{id} выполняется немедленно и прекращает продление. Вместе с линией удаляются сообщения, поэтому сначала прочитайте их.
  • Следите за number.grace. Это единственное предупреждение перед автоматическим освобождением линии, и оно приходит за 3 дней.
sweep.sh
# освободить все линии с меткой "signup-bot", неактивные в течение недели
curl -s "https://api.incognum.com/v1/numbers?label=signup-bot" \
     -H "Authorization: Bearer $NKP_KEY" |
jq -r '.data[] | select(.last_message_at < (now - 604800 | todate)) | .id' |
while read -r id; do
  curl -s -X DELETE "https://api.incognum.com/v1/numbers/$id" \
       -H "Authorization: Bearer $NKP_KEY"
done

Ограничения составляют 120 запросов чтения и 30 запросов записи в минуту на ключ; для заказов действует отдельный предел. Каждый ответ содержит X-RateLimit-Remaining. Получив 429, соблюдайте Retry-After, а не отправляйте запросы без остановки. Все значения приведены в справочнике API.

Частые вопросы

Обязательно ли использовать вебхуки или достаточно опроса?

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

Почему не проходит проверка подписи?

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

Можно ли назначить отдельный вебхук каждому номеру?

Да. URL вебхука для отдельной линии, заданный запросом PATCH к номеру, имеет приоритет над общим конечным адресом учётной записи. Это удобный способ направлять разные автоматизации в разные сервисы.

Что произойдёт, если мой сервер не работает?

Мы сделаем восемь повторных попыток примерно за сутки. После трёх дней непрерывных сбоев конечный адрес отключается, и панель сообщает об этом. Сообщения в любом случае не теряются: все они остаются доступными через API.

Есть ли официальный SDK?

Нет, и это сознательное решение. Интерфейс настолько мал, что оболочка устаревала бы быстрее самих HTTP-запросов. В справочнике API приведён полный клиент для Node и Python — примерно по пятнадцать строк на каждый.


Материал подготовила команда, управляющая этим сервисом, и обновила 21.05.2026.

Четыре запроса —
и дальше всё работает само.

Создайте ключ в панели и закажите первую линию из собственного кода. Документы ни на одном этапе не требуются.