> ## 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

> Start a Run with POST to this Instance.

A **Webhook** trigger starts a Run when your server sends HTTP POST to this Instance. A Workflow can have one active Webhook trigger.

<Steps>
  <Step title="Add the node">
    On the Graph, **Add a node** → **Webhook**. Connect it to the next step.
  </Step>

  <Step title="Publish and Start">
    Publish, then **Start** on [Overview](/instance/overview#webhook). Copy **Endpoint URL**. **Rotate secret** shows **Signing secret** once — copy it too. The secret is not shown again.
  </Step>
</Steps>

<h2 id="input">
  Input
</h2>

Inspector **Settings**. Empty-field rules for the Graph: [Previous nodes](/graph/previous-nodes). URL and signing secret are on Overview, not on this node.

| Field               | Required | Empty                       | Notes                                                                                                                                                            |
| ------------------- | -------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Kind**            | Yes      | —                           | **Webhook**. One active Webhook trigger per Workflow.                                                                                                            |
| **Run concurrency** | Yes      | Default: Parallel (default) | **Parallel (default)** (several POSTs may run at once) or **Serial (one delivery at a time)** (a second POST waits; you get 429 until the current Run finishes). |
| **Inbound files**   | No       | Default: Artifact only      | Multipart files. [Inbound files](/graph/triggers#inbound-files).                                                                                                 |

<h2 id="output">
  Output
</h2>

Menu and empty fields: [Previous nodes](/graph/previous-nodes). Tokens below: one Trigger on the Graph (`Trigger`). JSON keys from the POST body are not extra rows — use Body, or parse it in [Code](/graph/tools/code).

| Output              | In menu | Token                        | Type       | Next node gets                                                                               |
| ------------------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
| Body                | Yes     | `{{Trigger.text}}`           | Text       | Delivery body as the Run sees it. Also the Result.                                           |
| Photos              | Yes     | `{{Trigger.photos}}`         | JSON array | Photo attachments. Not a **File id**. [Photos and Files](/graph/previous-nodes#attachments). |
| Files               | Yes     | `{{Trigger.files}}`          | JSON array | File attachments.                                                                            |
| Thread              | Yes     | `{{Trigger.thread}}`         | Text       | Conversation thread you sent, or empty.                                                      |
| Delivery ID         | Yes     | `{{Trigger.deliveryId}}`     | Text       | This delivery.                                                                               |
| Content type        | Yes     | `{{Trigger.contentType}}`    | Text       | Request content type.                                                                        |
| Idempotency key     | Yes     | `{{Trigger.idempotencyKey}}` | Text       | `Idempotency-Key` header.                                                                    |
| Memory / State rows | No      | —                            | —          | Written by [Inbound files](/graph/triggers#inbound-files), not Previous nodes.               |

<h2 id="call">
  Call
</h2>

Send `POST` to the Endpoint URL with a JSON body. That JSON is what the Run sees.

The Instance must be **Watching**. If you pressed **Stop**, POST returns 503. Start on Overview, then retry.

**202** means the POST was accepted and a Run was queued — not that the Agent already finished.

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

`id` is this delivery (use it to check status). `runId` is the Run. `thread` is the conversation id you sent, or `null`.

In the snippets, paste **Endpoint URL** into `url` (the whole `https://…/api/hooks/…` string). Paste **Signing secret** into `secret`.

<h3 id="auth">
  Who is calling
</h3>

The Signing secret proves the POST is yours. Pick one:

* **Bearer** — send the secret in `Authorization`. Fastest way to get a 202.
* **HMAC** — do not put the secret in `Authorization`. Sign the body instead. See [HMAC](#hmac).

<h3 id="idempotency">
  Same request twice
</h3>

Every POST must include header `Idempotency-Key`. You invent the value — typically the id of the event in your system, such as `order-123`. Up to 256 characters.

If the HTTP client retries, send the **same** key and the **same** JSON. Riggery returns the same 202 and does not start a second Run.

The same key with **different** JSON returns 409. Use a new key for a new event.

<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>

Use HMAC when you do not want the secret in `Authorization`. It also proves the JSON was not changed on the way.

Sign these bytes, in order: the current unix time (seconds), a single dot, then the **exact** body you send (an extra space or newline breaks the check). HMAC-SHA256, hex digest. Header:

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

`TIME` must be within 5 minutes of the server clock.

```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` keeps the body identical to what you signed.

<h2 id="thread">
  One-shot or conversation
</h2>

With no `thread`, each POST is separate. The Agent does not see earlier webhook POSTs. The 202 field `thread` is `null`.

To keep a conversation (later POSTs share history), pick an id such as `crm-42` and send it every time: header `X-Webhook-Thread` or JSON field `thread`. Letters, numbers, `.`, `_`, `-`; 1–128 characters. Do not use `event`. If you set both header and JSON, they must be the same string.

<h2 id="files">
  Files
</h2>

To attach files, POST `multipart/form-data`: field `payload` is the JSON as a string; files use form names `files` or `file`. For HMAC, sign the full multipart body, not only the JSON.

<h2 id="delivery">
  Check the Run
</h2>

**202** is not the Agent's answer. It only means Riggery accepted this POST and started a Run. If your server must know whether the Workflow finished and what it returned, look up that POST by the `id` in the 202 JSON.

The lookup URL is the **Endpoint URL** with `/deliveries/` and the `id` appended. Example: you POST to `https://riggery.dev/api/hooks/xxxxxxxx` and the 202 body has `"id": "cldelivery01"`. You then `GET`:

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

Same **Bearer** or **HMAC** as the POST. For HMAC, sign an empty body (this GET has no JSON).

Call this URL again until `run.status` is `succeeded` or `failed`. If a Graph HTTP request is waiting for a person to confirm, status stays `waiting_approval`.

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

**200** looks like:

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

`run.output.text` is the result when the Run succeeded. `run.error` is set when it failed. While the Run is still going, `status` is `queued` or `running` and those fields are empty.

<h2 id="rotate">
  Rotate URL or secret
</h2>

**Rotate secret** if the secret leaked. The Endpoint URL stays. Old Bearer and HMAC requests fail. Copy the new **Signing secret** — it is shown once.

**Rotate URL** when you need a new Endpoint URL. Callers must POST to the new address; the previous URL stops accepting requests. The signing secret stays until you rotate it too.
