> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://agentinbox.space/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://agentinbox.space/_mcp/server.

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

> Проверка подписей вебхуков для защиты эндпоинтов от поддельных запросов.

При приёме вебхуков важно убедиться, что запрос пришёл от Agent Inbox и не был изменён. Доставка идёт через [Svix](https://www.svix.com/) с криптографической проверкой подписи.

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

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

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

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

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

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

```python
from agentinbox import Agentinbox

client = Agentinbox()

# Детали вебхука, включая секрет
webhook = client.webhooks.get(webhook_id="ep_xxx")

# Секрет начинается с "whsec_"
signing_secret = webhook.secret
print(f"Signing secret: {signing_secret}")
```

```typescript
import Agentinbox from "agentinbox";

const client = new Agentinbox();

const webhook = await client.webhooks.get("ep_xxx");

const signingSecret = webhook.secret;
console.log(`Signing secret: ${signingSecret}`);
```

**`CLI`**

```bash title="CLI"
# детали вебхука, включая секрет
agentinbox webhooks retrieve --webhook-id ep_xxx
```

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

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

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

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

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

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

```python
import os
from dotenv import load_dotenv
from flask import Flask, request

from svix.webhooks import Webhook, WebhookVerificationError

load_dotenv()

app = Flask(__name__)

secret = os.environ["AGENTINBOX_WEBHOOK_SECRET"]

@app.route('/webhooks', methods=['POST'])
def webhook_handler():
    headers = request.headers
    payload = request.get_data()

    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError as e:
        return ('', 400)

    # Разбор по event_type (msg — проверенный payload)
    if msg.get("event_type") == "message.received":
        # обработка входящего письма...
        pass
    elif msg.get("event_type") == "domain.verified":
        # включение функций домена...
        pass

    return ('', 204)

if __name__ == '__main__':
    app.run(port=3000)

```

```typescript
import "dotenv/config";
import express, { Request, Response } from "express";
import { Webhook } from "svix";

const app = express();
const port = 3000;

const secret = process.env.AGENTINBOX_WEBHOOK_SECRET;

if (!secret) {
  throw new Error("AGENTINBOX_WEBHOOK_SECRET environment variable is required");
}

app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  (req: Request, res: Response) => {
    const payload = req.body;
    const headers = req.headers as Record<string, string>;

    try {
      const wh = new Webhook(secret);
      const msg = wh.verify(payload, headers);

      if (msg.event_type === "message.received") {
        // обработка входящего письма...
      } else if (msg.event_type === "domain.verified") {
        // включение функций домена...
      }

      res.status(204).send();
    } catch (err) {
      console.error("Webhook verification failed:", err);
      res.status(400).send();
    }
  }
);

app.listen(port, () => {
  console.log(`Webhook server listening on port ${port}`);
});
```

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

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

**`Python`**

```python title="Python"
"""
Agent Inbox Webhook Verification — copy into Cursor/Claude.

Use Svix: pip install svix. Get secret from webhooks.get(webhook_id).secret (starts whsec_).
Headers: svix-id, svix-timestamp, svix-signature. Use request.get_data() (raw body), not request.json.
"""
from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError

app = Flask(__name__)
secret = "whsec_..."  # from client.webhooks.get(id).secret

@app.route("/webhooks", methods=["POST"])
def handler():
  try:
    wh = Webhook(secret)
    msg = wh.verify(request.get_data(), request.headers)
    if msg.get("event_type") == "message.received":
      pass  # process
    return "", 204
  except WebhookVerificationError:
    return "", 400
```

**`TypeScript`**

```typescript title="TypeScript"
/**
 * Agent Inbox Webhook Verification — copy into Cursor/Claude.
 *
 * Use Svix: npm install svix. Secret from webhooks.get(id).secret.
 * Use express.raw({ type: "application/json" }) — NOT express.json(). Verify before parsing.
 */
import express from "express";
import { Webhook } from "svix";

const app = express();
const secret = process.env.AGENTINBOX_WEBHOOK_SECRET;
if (!secret) {
  throw new Error("AGENTINBOX_WEBHOOK_SECRET environment variable is required");
}

app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const wh = new Webhook(secret);
    const msg = wh.verify(req.body, req.headers as Record<string, string>);
    if (msg.event_type === "message.received") { /* process */ }
    res.status(204).send();
  } catch {
    res.status(400).send();
  }
});
```

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

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

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

**`webhook_server.py`**

```python title="webhook_server.py"
import os
from dotenv import load_dotenv
from flask import Flask, request

from svix.webhooks import Webhook, WebhookVerificationError

load_dotenv()

app = Flask(__name__)

secret = os.environ.get("AGENTINBOX_WEBHOOK_SECRET")

@app.route('/webhooks', methods=['POST'])
def webhook_handler():
    headers = request.headers
    payload = request.get_data()

    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
        print(f"Received event: {msg}")
    except WebhookVerificationError as e:
        print(f"Verification failed: {e}")
        return ('', 400)

    if msg.get("event_type") == "message.received":
        pass
    elif msg.get("event_type") == "domain.verified":
        pass

    return ('', 204)

if __name__ == '__main__':
    app.run(port=3000)
```

**`webhook_server.ts`**

```typescript title="webhook_server.ts"
import "dotenv/config";
import express, { Request, Response } from "express";
import { Webhook } from "svix";

const app = express();
const port = 3000;

const secret = process.env.AGENTINBOX_WEBHOOK_SECRET;

if (!secret) {
  throw new Error("AGENTINBOX_WEBHOOK_SECRET environment variable is required");
}

app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  (req: Request, res: Response) => {
    const payload = req.body;
    const headers = req.headers as Record<string, string>;

    try {
      const wh = new Webhook(secret);
      const msg = wh.verify(payload, headers);

      if (msg.event_type === "message.received") {
      } else if (msg.event_type === "domain.verified") {
      }

      res.status(204).send();
    } catch (err) {
      console.error("Webhook verification failed:", err);
      res.status(400).send();
    }
  }
);

app.listen(port, () => {
  console.log(`Webhook server listening on port ${port}`);
});
```

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

**`Python`**

```bash title="Python"
pip install flask python-dotenv svix
python webhook_server.py
```

**`TypeScript`**

```bash title="TypeScript"
npm install express svix dotenv
npx ts-node webhook_server.ts
```

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

**`Python`**

```bash title="Python"
 * Serving Flask app 'webhook_server'
 * Debug mode: off
 * Running on http://127.0.0.1:3000
Press CTRL+C to quit
```

**`TypeScript`**

```bash title="TypeScript"
Webhook server listening on port 3000
```

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

```bash
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](https://console.agentinbox.space)
2. Раздел **Webhooks**
3. **Create Webhook** (или редактирование)
4. URL с путём `/webhooks`, например `https://da550b82a183.ngrok.app/webhooks`
5. Выберите события, сохраните
6. Секрет подписи в `.env`:

```bash
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](https://render.com/).

> **Другие платформы**
>
> Также подойдут [Railway](https://railway.app/), [Fly.io](https://fly.io/), [Heroku](https://heroku.com/) или любой облачный хостинг с Python и HTTPS.

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

#### В проде всегда проверяйте

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

#### Переменные окружения

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

```python
import os
WEBHOOK_SECRET = os.environ["AGENTINBOX_WEBHOOK_SECRET"]
```

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

#### Подпись не сходится

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

#### Нет заголовков

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

#### Парсинг тела

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

#### [Настройка вебхуков](/webhook-setup)

Полное руководство с ngrok.

#### [События вебхуков](/events)

Все типы событий.