> ## Documentation Index
> Fetch the complete documentation index at: https://docs.riggery.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook

> Запустите Run запросом POST на этот Instance.

Триггер **Webhook** начинает запуск, когда ваш сервер отправляет HTTP POST на этот Instance. У Workflow может быть один активный триггер Webhook.

<Steps>
  <Step title="Добавьте узел">
    На графе **Добавить узел** → **Webhook**. Соедините со следующим шагом.
  </Step>

  <Step title="Опубликуйте и Запустите">
    Опубликуйте, затем **Запустить** на [Обзоре](/ru/instance/overview#webhook). Скопируйте **URL эндпоинта**. **Сменить секрет** один раз показывает **Секрет подписи** — скопируйте и его. Секрет больше не показывается.
  </Step>
</Steps>

<h2 id="input">
  Вход
</h2>

Вкладка инспектора **Настройки**. Правила пустого поля на графе: [Предыдущие узлы](/ru/graph/previous-nodes). URL и секрет подписи — на Обзоре, не на этом узле.

| Поле                        | Обязательно | Пустое                                   | Заметки                                                                                                                                                                           |
| --------------------------- | ----------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Тип**                     | Да          | —                                        | **Webhook**. Один активный триггер Webhook на Workflow.                                                                                                                           |
| **Параллельность запусков** | Да          | По умолчанию: Параллельно (по умолчанию) | **Параллельно (по умолчанию)** (несколько POST могут идти сразу) или **Последовательно (одна доставка за раз)** (второй POST ждёт; пока текущий запуск не закончится, ответ 429). |
| **Входящие файлы**          | Нет         | По умолчанию: Только файл                | Файлы multipart. [Входящие файлы](/ru/graph/triggers#inbound-files).                                                                                                              |

<h2 id="output">
  Выход
</h2>

Меню и пустое поле: [Предыдущие узлы](/ru/graph/previous-nodes). Токены ниже — один триггер на графе (`Trigger`). Ключи JSON из POST не становятся отдельными строками — берите Body или разберите его в [Code](/ru/graph/tools/code).

| Выход                 | В меню | Токен                        | Тип         | Следующий узел получит                                                         |
| --------------------- | ------ | ---------------------------- | ----------- | ------------------------------------------------------------------------------ |
| Body                  | Да     | `{{Trigger.text}}`           | Текст       | Тело доставки, как его видит запуск. Также Result.                             |
| Photos                | Да     | `{{Trigger.photos}}`         | JSON-массив | Фото. Не **ID файла**. [Photos и Files](/ru/graph/previous-nodes#attachments). |
| Files                 | Да     | `{{Trigger.files}}`          | JSON-массив | Файлы.                                                                         |
| Thread                | Да     | `{{Trigger.thread}}`         | Текст       | Диалог, который вы передали, или пусто.                                        |
| Delivery ID           | Да     | `{{Trigger.deliveryId}}`     | Текст       | Эта доставка.                                                                  |
| Content type          | Да     | `{{Trigger.contentType}}`    | Текст       | Content type запроса.                                                          |
| Idempotency key       | Да     | `{{Trigger.idempotencyKey}}` | Текст       | Заголовок `Idempotency-Key`.                                                   |
| Строки Memory / State | Нет    | —                            | —           | Пишет [Входящие файлы](/ru/graph/triggers#inbound-files), не Предыдущие узлы.  |

<h2 id="call">
  Вызов
</h2>

Отправьте `POST` на URL эндпоинта с телом JSON. Этот JSON видит запуск.

Instance должен быть в статусе **Слушает**. Если нажали **Остановить**, POST вернёт 503. Снова **Запустить** на Обзоре, затем повторите запрос.

**202** значит: POST принят и запуск поставлен в очередь. Агент к этому моменту ещё может не закончить.

```json theme={null}
{
  "id": "delivery-id",
  "runId": "run-id",
  "status": "accepted",
  "thread": null
}
```

`id` — эта доставка (по нему смотрят статус). `runId` — запуск. `thread` — id разговора, который вы прислали, или `null`.

В примерах в `url` вставьте **URL эндпоинта** целиком (`https://…/api/hooks/…`). В `secret` — **Секрет подписи**.

<h3 id="auth">
  Кто вызывает
</h3>

Секрет подписи доказывает, что POST ваш. Один из двух способов:

* **Bearer** — секрет в заголовке `Authorization`. Так проще получить первый 202.
* **HMAC** — секрет в `Authorization` не кладёте. Подписываете тело. См. [HMAC](#hmac).

<h3 id="idempotency">
  Один запрос дважды
</h3>

В каждом POST нужен заголовок `Idempotency-Key`. Значение придумываете вы — обычно id события у вас в системе, например `order-123`. Не длиннее 256 символов.

Если HTTP-клиент повторил тот же запрос, отправьте **тот же** ключ и **тот же** JSON. Riggery вернёт тот же 202 и не начнёт второй запуск.

Тот же ключ с **другим** JSON — ответ 409. Для нового события — новый ключ.

<CodeGroup>
  ```bash theme={null}
  URL='https://riggery.dev/api/hooks/xxxxxxxx'
  SECRET='xxxxxxxx'

  curl -sS -X POST "$URL" \
    -H "Authorization: Bearer $SECRET" \
    -H "Idempotency-Key: order-123" \
    -H "Content-Type: application/json" \
    --data-binary '{"orderId":"123"}'
  ```

  ```python theme={null}
  import json, urllib.request

  url = "https://riggery.dev/api/hooks/xxxxxxxx"
  secret = "xxxxxxxx"
  body = json.dumps({"orderId": "123"}).encode()

  req = urllib.request.Request(
      url,
      data=body,
      method="POST",
      headers={
          "Authorization": f"Bearer {secret}",
          "Idempotency-Key": "order-123",
          "Content-Type": "application/json",
      },
  )
  print(urllib.request.urlopen(req).read().decode())
  ```

  ```javascript theme={null}
  const url = "https://riggery.dev/api/hooks/xxxxxxxx";
  const secret = "xxxxxxxx";

  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${secret}`,
      "Idempotency-Key": "order-123",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ orderId: "123" }),
  });
  console.log(await res.json());
  ```

  ```go theme={null}
  package main

  import (
  	"fmt"
  	"io"
  	"net/http"
  	"strings"
  )

  func main() {
  	url := "https://riggery.dev/api/hooks/xxxxxxxx"
  	secret := "xxxxxxxx"
  	body := `{"orderId":"123"}`

  	req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
  	req.Header.Set("Authorization", "Bearer "+secret)
  	req.Header.Set("Idempotency-Key", "order-123")
  	req.Header.Set("Content-Type", "application/json")
  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer res.Body.Close()
  	out, _ := io.ReadAll(res.Body)
  	fmt.Println(res.Status, string(out))
  }
  ```

  ```php theme={null}
  <?php
  $url = 'https://riggery.dev/api/hooks/xxxxxxxx';
  $secret = 'xxxxxxxx';
  $body = '{"orderId":"123"}';

  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
      'Authorization: Bearer ' . $secret,
      'Idempotency-Key: order-123',
      'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
  ]);
  echo curl_exec($ch);
  ```
</CodeGroup>

<h2 id="hmac">
  HMAC
</h2>

HMAC — если секрет не хотите класть в `Authorization`. Заодно проверяется, что JSON по дороге не меняли.

Подпишите подряд: текущее unix-время в секундах, одну точку, затем **ровно** то тело, которое уйдёт в POST (лишний пробел или перевод строки ломает проверку). Алгоритм HMAC-SHA256, результат — hex. Заголовок:

`X-Riggery-Signature: t=TIME,v1=HEX`

`TIME` не старше 5 минут относительно часов сервера.

```bash theme={null}
URL='https://riggery.dev/api/hooks/xxxxxxxx'
SECRET='xxxxxxxx'
BODY='{"orderId":"123"}'
TS="$(date +%s)"
SIG="$(printf '%s%s' "${TS}." "${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $NF}')"

curl -sS -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-123" \
  -H "X-Riggery-Signature: t=${TS},v1=${SIG}" \
  --data-binary "$BODY"
```

`--data-binary` отправляет то же тело, которое вы подписали.

<h2 id="thread">
  Разово или диалог
</h2>

Без `thread` каждый POST сам по себе. Агент не видит прошлые webhook-запросы. В 202 поле `thread` равно `null`.

Чтобы это был один разговор (следующие POST видят историю), выберите id вроде `crm-42` и шлите его каждый раз: заголовок `X-Webhook-Thread` или поле JSON `thread`. Буквы, цифры, `.`, `_`, `-`; от 1 до 128 символов. Слово `event` нельзя. Если заданы и заголовок, и JSON — строки должны совпасть.

<h2 id="files">
  Файлы
</h2>

Чтобы приложить файлы, POST `multipart/form-data`: поле `payload` — JSON как строка; файлы — имена полей `files` или `file`. Для HMAC подписывайте всё multipart-тело, не только JSON.

<h2 id="delivery">
  Проверить запуск
</h2>

**202** — это ещё не ответ агента. Только то, что Riggery принял этот POST и начал запуск. Если вашему серверу нужно знать, закончился ли Workflow и что он вернул, найдите этот POST по полю `id` в JSON с кодом 202.

Адрес — тот же **URL эндпоинта**, к которому добавлены `/deliveries/` и `id`. Пример: POST был на `https://riggery.dev/api/hooks/xxxxxxxx`, в 202 пришло `"id": "cldelivery01"`. Тогда `GET`:

`https://riggery.dev/api/hooks/xxxxxxxx/deliveries/cldelivery01`

Тот же **Bearer** или **HMAC**, что у POST. Для HMAC подпись считается по пустому телу (у этого GET нет JSON).

Повторяйте этот URL, пока `run.status` не станет `succeeded` или `failed`. Если на графе HTTP request ждёт подтверждения человеком, статус останется `waiting_approval`.

```bash theme={null}
curl -sS "$URL/deliveries/cldelivery01" \
  -H "Authorization: Bearer $SECRET"
```

**200** выглядит так:

```json theme={null}
{
  "id": "cldelivery01",
  "runId": "run-id",
  "thread": null,
  "run": {
    "status": "succeeded",
    "output": { "text": "…" },
    "error": null
  }
}
```

`run.output.text` — результат, если запуск успел. `run.error` — если нет. Пока запуск ещё идёт, `status` равен `queued` или `running`, эти поля пустые.

<h2 id="rotate">
  Сменить URL или секрет
</h2>

**Сменить секрет**, если секрет попал к чужим. URL эндпоинта тот же. Старые Bearer и HMAC перестают работать. Новый **Секрет подписи** копируют сразу — он показывается один раз.

**Сменить URL**, когда нужен новый URL эндпоинта. Дальше POST идёт на новый адрес; прежний запросы больше не принимает. Секрет не меняется, пока его отдельно не смените.
