Проверка вебхуков

Убедитесь, что запросы действительно от Agent Inbox.

При приёме вебхуков важно убедиться, что запрос пришёл от Agent Inbox и не был изменён. Доставка идёт через Svix с криптографической проверкой подписи.

Зачем проверять вебхуки?

Без проверки любой, кто узнает URL, может слать поддельные события:

  • Подмена данных: ложные события запускают действия в вашей системе.
  • Инциденты безопасности: вредоносные данные в потоке обработки.
  • Исчерпание ресурсов: флуд фейковыми запросами.

В продакшене проверку подписи включайте всегда.

Секрет подписи

У каждого вебхука свой секрет. Его видно в консоли Agent Inbox при создании или в ответе API:

1from agentinbox import Agentinbox
2
3client = Agentinbox()
4
5# Детали вебхука, включая секрет
6webhook = client.webhooks.get(webhook_id="ep_xxx")
7
8# Секрет начинается с "whsec_"
9signing_secret = webhook.secret
10print(f"Signing secret: {signing_secret}")
Храните секрет в безопасности

Держите секрет в переменных окружения или секрет-хранилище. Не коммитьте в репозиторий и не отдавайте в клиентский код.

Заголовки проверки

В каждом запросе от Agent Inbox есть три заголовка Svix:

ЗаголовокОписание
svix-idУникальный ID сообщения. Тот же ID при ретраях.
svix-timestampUnix-время (секунды) отправки.
svix-signatureПодписи через пробел: v1,<base64> (например v1,abc123 v1,def456).

Библиотека Svix (рекомендуется)

Проще всего — официальная библиотека Svix.

1import os
2from dotenv import load_dotenv
3from flask import Flask, request
4
5from svix.webhooks import Webhook, WebhookVerificationError
6
7load_dotenv()
8
9app = Flask(__name__)
10
11secret = os.environ["AGENTINBOX_WEBHOOK_SECRET"]
12
13@app.route('/webhooks', methods=['POST'])
14def webhook_handler():
15 headers = request.headers
16 payload = request.get_data()
17
18 try:
19 wh = Webhook(secret)
20 msg = wh.verify(payload, headers)
21 except WebhookVerificationError as e:
22 return ('', 400)
23
24 # Разбор по event_type (msg — проверенный payload)
25 if msg.get("event_type") == "message.received":
26 # обработка входящего письма...
27 pass
28 elif msg.get("event_type") == "domain.verified":
29 # включение функций домена...
30 pass
31
32 return ('', 204)
33
34if __name__ == '__main__':
35 app.run(port=3000)
Нужно сырое тело

Проверка подписи требует точного тела запроса. Если используете парсер тела (express.json() и т.п.), для маршрута вебхука берите raw-тело или используйте express.raw().

Скопировать в Cursor / Claude

1"""
2Agent Inbox Webhook Verification — copy into Cursor/Claude.
3
4Use Svix: pip install svix. Get secret from webhooks.get(webhook_id).secret (starts whsec_).
5Headers: svix-id, svix-timestamp, svix-signature. Use request.get_data() (raw body), not request.json.
6"""
7from flask import Flask, request
8from svix.webhooks import Webhook, WebhookVerificationError
9
10app = Flask(__name__)
11secret = "whsec_..." # from client.webhooks.get(id).secret
12
13@app.route("/webhooks", methods=["POST"])
14def handler():
15 try:
16 wh = Webhook(secret)
17 msg = wh.verify(request.get_data(), request.headers)
18 if msg.get("event_type") == "message.received":
19 pass # process
20 return "", 204
21 except WebhookVerificationError:
22 return "", 400

Локальная проверка с ngrok

Чтобы Agent Inbox достучался до локального сервера, нужен публичный URL. ngrok даёт туннель на вашу машину.

Шаг 1: Сохраните сервер

1import os
2from dotenv import load_dotenv
3from flask import Flask, request
4
5from svix.webhooks import Webhook, WebhookVerificationError
6
7load_dotenv()
8
9app = Flask(__name__)
10
11secret = os.environ.get("AGENTINBOX_WEBHOOK_SECRET")
12
13@app.route('/webhooks', methods=['POST'])
14def webhook_handler():
15 headers = request.headers
16 payload = request.get_data()
17
18 try:
19 wh = Webhook(secret)
20 msg = wh.verify(payload, headers)
21 print(f"Received event: {msg}")
22 except WebhookVerificationError as e:
23 print(f"Verification failed: {e}")
24 return ('', 400)
25
26 if msg.get("event_type") == "message.received":
27 pass
28 elif msg.get("event_type") == "domain.verified":
29 pass
30
31 return ('', 204)
32
33if __name__ == '__main__':
34 app.run(port=3000)

Шаг 2: Зависимости и запуск

$pip install flask python-dotenv svix
$python webhook_server.py

Ожидаемый вывод:

$ * Serving Flask app 'webhook_server'
$ * Debug mode: off
$ * Running on http://127.0.0.1:3000
$Press CTRL+C to quit

Шаг 3: Запуск ngrok

$ngrok http 3000

Пример вывода:

Session Status online
Account your-email@example.com (Plan: Free)
Version 3.22.1
Region United States (California) (us-cal-1)
Forwarding https://da550b82a183.ngrok.app -> http://localhost:3000

Скопируйте HTTPS forwarding URL (например https://da550b82a183.ngrok.app).

Шаг 4: URL в консоли Agent Inbox

  1. Консоль Agent Inbox
  2. Раздел Webhooks
  3. Create Webhook (или редактирование)
  4. URL с путём /webhooks, например https://da550b82a183.ngrok.app/webhooks
  5. Выберите события, сохраните
  6. Секрет подписи в .env:
$AGENTINBOX_WEBHOOK_SECRET=whsec_your_secret_here

Шаг 5: Тестовое событие

Отправьте письмо на один из inbox или тест из консоли. В терминале сервера должно появиться:

127.0.0.1 - - [19/Jan/2026 16:57:20] "POST /webhooks HTTP/1.1" 204 -
Received event: {'event_type': 'message.received', ...}
Продакшен

Ngrok удобен локально; в проде разверните сервер на хостинге со стабильным HTTPS. Варианты ниже.

Продакшен

Нужен стабильный публичный HTTPS и переменные окружения. Удобный старт — Render.

Другие платформы

Также подойдут Railway, Fly.io, Heroku или любой облачный хостинг с Python и HTTPS.

Рекомендации

Локально проверку можно ослабить, в продакшене — обязательно. Скомпрометированный вебхук — серьёзный риск.

Секрет не хардкодьте:

1import os
2WEBHOOK_SECRET = os.environ["AGENTINBOX_WEBHOOK_SECRET"]

Устранение неполадок

  • Используйте сырое тело, не перепарсенное
  • Секрет совпадает с эндпоинтом в консоли
  • Заголовки читаются корректно (регистр не важен)
  • Время в пределах допуска (по умолчанию около 5 минут)

Прокси или фреймворк могут отрезать кастомные заголовки — проверьте настройки reverse proxy.

Для Express на маршруте вебхука используйте express.raw().