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

# Webhooks

> Let external systems trigger a flow definition by POSTing HMAC-signed deliveries to a per-customer receive URL

Inbound webhooks let external systems — Five9, Stripe, anything that can POST — trigger a [definition](/concepts/flow-definitions). An admin creates a webhook config bound to an active definition; the external sender POSTs signed deliveries to a per-customer receive URL; each verified delivery starts a normal [job](/concepts/jobs-and-executions) (`source: "webhook"`, with `webhook_id` + `delivery_id` in the job metadata).

<Note>
  This surface is **inbound-only**. Outbound webhooks — workflow tasks calling external URLs — are a separate future REST catalog action, not receiver machinery.
</Note>

```json theme={null}
{
  "id": "8a9c86a8b78c48d3a3e5e00ad0ed5b96",
  "definition_id": "def_a1b2c3d4",
  "description": "Five9 call-completed events",
  "mode": "trigger",
  "provider": "standard",
  "enabled": true,
  "receive_url": "/webhooks/acme/8a9c86a8b78c48d3a3e5e00ad0ed5b96"
}
```

## The webhook object

| Field            | Type            | Description                                                                                                                                                                                                     |
| ---------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`             | string          | 128-bit opaque hex identifier — the receive URL is unguessable by construction.                                                                                                                                 |
| `definition_id`  | string          | Definition to trigger. Must belong to your org and be re-validated as `active` at dispatch time.                                                                                                                |
| `org_id`         | string          | Owning organization.                                                                                                                                                                                            |
| `description`    | string          | Human-readable description (1–200 chars).                                                                                                                                                                       |
| `mode`           | string          | `trigger` (each delivery starts a new job). `resume` is reserved and rejected with `422`. Immutable after create.                                                                                               |
| `provider`       | string          | Signature-verification adapter. Only `standard` ([Standard Webhooks](https://www.standardwebhooks.com/) HMAC-SHA256) is implemented; `stripe`/`five9`/`custom` are rejected with `422`. Immutable after create. |
| `input_template` | object \| null  | Optional `{{ }}`-expression mapping from the normalized `payload` context to the definition's `input_data`. Omit to pass the whole normalized payload through.                                                  |
| `dispatch_retry` | object \| null  | Admin-authored dispatch retry policy, default OFF (one attempt), e.g. `{"max_attempts": 3, "backoff": 5}`. Never read from the sender's body.                                                                   |
| `rate_limit`     | integer \| null | Per-webhook deliveries/minute cap. Default `1000`.                                                                                                                                                              |
| `enabled`        | boolean         | `false` makes the receive URL answer `404`.                                                                                                                                                                     |
| `signing_secret` | string          | **Show-once.** Returned only on create and rotate — never retrievable from any `GET`. Store it in the sending system immediately.                                                                               |
| `receive_url`    | string          | **Show-once** relative path. Prepend `https://api.getdialed.ai/flows` before handing it to a sender.                                                                                                            |

## The receive URL

The receiver path is `POST /webhooks/{slug}/{webhook_id}`. In production the Flows API mounts under the `/flows` ingress prefix, so the URL external senders must call is:

```
https://api.getdialed.ai/flows/webhooks/{slug}/{webhook_id}
```

The `receive_url` returned at create time is a **relative path** (no host, no `/flows` prefix) — prepend `https://api.getdialed.ai/flows` when configuring your sender. The `{slug}` identifies your tenant; the 128-bit `{webhook_id}` is unguessable.

## Signature verification

<Warning>
  The receiver carries **no** `X-API-Key` / `Authorization` requirement — the trust boundary is the **HMAC-SHA256 signature over the raw request body**, verified per the [Standard Webhooks](https://www.standardwebhooks.com/) spec **before the body is parsed**. Every sender must sign each delivery with the webhook's signing secret, or the delivery is rejected with `401`.
</Warning>

Senders include three Standard Webhooks headers on every delivery:

| Header              | Description                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | Sender-chosen unique delivery ID. **This is the idempotency key** — re-sending the same value is safe.         |
| `webhook-timestamp` | Unix seconds. Deliveries outside the **5-minute replay window** are rejected with `401`.                       |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>` computed over `{webhook-id}.{webhook-timestamp}.{raw body}` with the signing secret. |

The `401` message is deliberately generic — a missing header, an expired timestamp, a tampered body, and a wrong secret all return the same `{"detail": "Signature verification failed"}`, revealing nothing to a probing caller. Existence failures (unknown slug, unknown `webhook_id`, or a disabled webhook) always return `404`, never `401`.

## Delivery semantics

* **202-async.** The receiver acknowledges within \~50ms with `202 Accepted`; job creation and dispatch happen asynchronously after the ack. Check the deliveries view for the outcome.
* **Deduplication.** Exactly one job per `webhook-id`. A replayed delivery returns `202` but does not dispatch again — sender retry storms collapse to one job.
* **Rate limit.** Each webhook accepts up to `rate_limit` deliveries/minute (default **1000/min**), counted per `webhook_id` (not per source IP, so senders sharing egress IPs don't throttle each other). Over-limit deliveries get `429`.
* **Body format.** `Content-Type` drives normalization — JSON is parsed, form-urlencoded becomes a dict, XML is parsed with a hardened parser, anything else is wrapped as `{"raw": ..., "content_type": ...}`. The receiver never rejects on format.

## Managing webhooks

All management endpoints require authentication and operate only on your organization's webhooks. Mutating endpoints require an admin-role caller (an `X-API-Key` is admin-equivalent).

| Method   | Endpoint                               | Purpose                                                                                                                                                |
| -------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST`   | `/webhooks`                            | Create a config and mint its signing secret. Returns `201` with the show-once `signing_secret` + `receive_url`.                                        |
| `GET`    | `/webhooks`                            | List your org's webhooks with cursor pagination. The secret never appears on the read model.                                                           |
| `GET`    | `/webhooks/{webhook_id}`               | Fetch one config (no secret, no receive URL).                                                                                                          |
| `PATCH`  | `/webhooks/{webhook_id}`               | Partial update — rebind `definition_id`, edit `description`/`input_template`/`dispatch_retry`/`rate_limit`/`enabled`. `mode`/`provider` are immutable. |
| `POST`   | `/webhooks/{webhook_id}/rotate-secret` | Rotate the signing secret with a 24-hour previous-secret overlap. Returns the new secret once.                                                         |
| `DELETE` | `/webhooks/{webhook_id}`               | Delete the config and scrub its stored signing secret. Returns `204`.                                                                                  |
| `GET`    | `/webhooks/{webhook_id}/deliveries`    | Recent deliveries — the debugging view for "did my sender's POST arrive and start a job?".                                                             |
| `POST`   | `/webhooks/{slug}/{webhook_id}`        | **The receiver** — called by your external system, HMAC-signed, no API key. Returns `202`.                                                             |

Creating one:

```bash theme={null}
curl -X POST "$BASE_URL/webhooks" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "definition_id": "def_a1b2c3d4",
    "description": "Five9 call-completed events"
  }'
```

<Tip>
  The `signing_secret` is returned exactly once — on create and on `rotate-secret`. Rotation keeps the previous secret valid for 24 hours so you can migrate senders without a hard cutover. If you lose a secret, rotate to mint a new one rather than recreating the webhook (which would change the receive URL).
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Receive webhooks" icon="webhook" href="/guides/receive-webhooks">
    A step-by-step walkthrough: create a webhook, sign a delivery, and confirm the job.
  </Card>

  <Card title="Event subscriptions" icon="bell" href="/concepts/event-subscriptions">
    Trigger definitions from internal business events instead of external POSTs.
  </Card>
</CardGroup>
