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

# Webhooks

> Receive HTTP notifications at your own URL for subscribed events

Webhooks let you receive real-time notifications at your own URL when subscribed events occur. Use them to forward events to your SIEM, build approval workflows, notify on-call, or trigger automations.

## Prerequisites

* **Unbound account** with Admin role
* A **public HTTPS endpoint** you control to receive the events
* (Optional) A **server-side secret manager** to store the signing secret

## Setup

1. In your Unbound dashboard, go to **Settings → Webhooks**
2. Click **Add Endpoint**
3. Enter your endpoint URL (must be `https://`, public, and not pointing at a private network)
4. Optionally add a description
5. Select one or more events to subscribe to (or **Select all**)
6. Click **Create**

On creation, your endpoint's `whsec_…` signing secret is shown **once**. Copy it now and store it in your secret manager. You can reveal it again from the endpoint detail page if you need to, but treat it like an API key.

## Signature verification

Every delivery includes three headers your receiver can use to verify the event came from Unbound and wasn't tampered with in transit.

| Header              | Description                                                                                                                         |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | Unique idempotency key (`msg_…`). Use it to deduplicate retries.                                                                    |
| `webhook-timestamp` | Unix seconds when we signed the payload. Reject deliveries more than \~5 minutes old to prevent replay.                             |
| `webhook-signature` | `v1,<base64-hmac-sha256>` computed over `${webhook-id}.${webhook-timestamp}.${raw-body}` using your signing secret as the HMAC key. |

The verification algorithm is standard HMAC-SHA256:

1. Strip the `whsec_` prefix from your signing secret and base64-decode the remainder. The result is the HMAC key.
2. Build the signed payload by concatenating `<webhook-id>.<webhook-timestamp>.<raw-body>`.
3. Compute `HMAC-SHA256(key, signed_payload)` and base64-encode the digest. Prefix it with `v1,`.
4. Compare against the `webhook-signature` header using a constant-time comparison. Multiple signatures may be space-separated; accept if any one matches.
5. Reject deliveries whose `webhook-timestamp` is older than 5 minutes to prevent replay.

<Note>
  Always verify against the **raw request body bytes**, not a re-serialised JSON object. JSON re-serialisation can reorder keys or change whitespace, breaking the signature.
</Note>

## Event types

Each command or tool call can fire multiple events. The four per-action events fire whenever a policy of that action matches, and `*.logged` fires for everything. A command that matched a Block policy therefore produces two deliveries: `*.blocked` and `*.logged`.

| Event                                       | Fires when                                    |
| ------------------------------------------- | --------------------------------------------- |
| `terminal_command.blocked`                  | A terminal command matched a Block policy     |
| `terminal_command.warned`                   | A terminal command matched a Warn policy      |
| `terminal_command.slack_approval_requested` | A terminal command is awaiting Slack approval |
| `terminal_command.audited`                  | A terminal command matched an Audit policy    |
| `terminal_command.logged`                   | Every terminal command observed by Unbound    |
| `mcp_tool.blocked`                          | An MCP tool call matched a Block policy       |
| `mcp_tool.warned`                           | An MCP tool call matched a Warn policy        |
| `mcp_tool.slack_approval_requested`         | An MCP tool call is awaiting Slack approval   |
| `mcp_tool.audited`                          | An MCP tool call matched an Audit policy      |
| `mcp_tool.logged`                           | Every MCP tool call observed by Unbound       |

You can subscribe to specific events, an entire group (`terminal_command.*` or `mcp_tool.*`), or all events (`*`). `*.logged` is the firehose — every classified command or tool call. Use it for SIEM streaming; pick the per-action events if you only want policy-driven activity.

## Payload

Every event uses the same envelope: a top-level `id` (ULID, prefixed `msg_`), `type` (the event name), `timestamp` (ISO-8601, UTC), and a nested `data` block. The `data` block is consistent across all event types within a family — terminal-command events carry a `command` string, MCP-tool events carry `mcp_server`, `mcp_tool`, and `mcp_parameters` instead, and everything else is shared.

A single command or tool call can trigger multiple events (e.g. a blocked command fires both `*.blocked` and `*.logged`). Each event is a separate delivery with its own `id` and signature, but they share the same `data.tool_use_id` so you can dedupe across events if you want one record per command.

### Field reference

| Field                | Type           | Description                                                                                                                                                                                                                                                                                |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tool_use_id`        | string         | Stable per-command identifier (prefixed `tu_`). The same value appears on every event fired for the same command — use it to dedupe across overlapping subscriptions (e.g. an endpoint subscribed to both `*.blocked` and `*.logged` receives two deliveries with the same `tool_use_id`). |
| `tool`               | string \| null | The AI tool that issued the call (e.g. `claude-code`, `cursor`, `codex`, `copilot`).                                                                                                                                                                                                       |
| `tool_name`          | string         | The specific tool name. For terminal commands this is `Bash`, `Edit`, etc.; for MCP calls it's `<server>__<tool>`.                                                                                                                                                                         |
| `user_email`         | string \| null | The email of the user who owns the application that fired the call.                                                                                                                                                                                                                        |
| `prompt`             | string \| null | The user's original prompt that led to this activity. Useful for context.                                                                                                                                                                                                                  |
| `thread_id`          | string \| null | Conversation thread ID for grouping related events.                                                                                                                                                                                                                                        |
| `intent_attribution` | string         | `USER_INTENTIONAL`, `AGENT_INITIATED`, or `UNKNOWN` — whether the user explicitly asked for this or the agent decided autonomously.                                                                                                                                                        |
| `command`            | string         | (terminal only) The literal command string.                                                                                                                                                                                                                                                |
| `mcp_server`         | string         | (MCP only) The MCP server the tool call targeted.                                                                                                                                                                                                                                          |
| `mcp_tool`           | string         | (MCP only) The specific tool within that server.                                                                                                                                                                                                                                           |
| `mcp_parameters`     | object         | (MCP only) The parsed arguments passed to the tool.                                                                                                                                                                                                                                        |
| `matched_policies`   | array          | Every policy that matched this event. Empty on `*.logged` events when no policy matched. Each entry contains `id`, `name`, `action`, `policy_type`.                                                                                                                                        |
| `classifications`    | array          | All command families this event was classified into (compound commands can produce multiple). Each entry has `command_family`, `confidence_score`, `targets`.                                                                                                                              |

## Retries

Failed deliveries (anything that isn't HTTP 200–299) retry on the following schedule:

| Attempt | Delay since previous |
| ------- | -------------------- |
| 1       | (immediate)          |
| 2       | 5 seconds            |
| 3       | 5 minutes            |
| 4       | 30 minutes           |
| 5       | 2 hours              |
| 6       | 5 hours              |
| 7       | 10 hours             |
| 8       | 10 hours             |

The retry horizon is approximately 28 hours total. Each attempt times out after **15 seconds**. After 8 failed attempts the event is dropped.

<Note>
  Redirects (`3xx`) are treated as failures and **not followed**. Configure your endpoint to be the resolved URL.
</Note>

## Custom headers

You can attach static custom headers to every delivery for your endpoint — useful when your receiver requires a specific authentication header.

| Receiver               | Header                                     |
| ---------------------- | ------------------------------------------ |
| Splunk HEC             | `Authorization: Splunk <hec-token>`        |
| Datadog Logs Intake    | `DD-API-KEY: <key>`                        |
| Sumo Logic HTTP Source | (no header — URL is the credential)        |
| Tines                  | (no header — URL secret is the credential) |
| Slack incoming webhook | (no header — URL is the credential)        |

To add custom headers, click your endpoint in the dashboard, then **Custom headers → Edit**. Reserved headers (`content-type`, `user-agent`, `host`, and our signature headers) cannot be overridden.

## Testing

To send a test event:

1. Open your endpoint from **Settings → Webhooks**
2. Click **Send test event**
3. Pick an event type from the dropdown
4. Click **Send**

The test event uses the same signing pipeline and headers as a real event. Failed test deliveries are **not retried** so you can iterate quickly on your receiver. Test deliveries appear in the **Message Attempts** table tagged with `test`.

## Managing endpoints

From the endpoint detail page you can:

* **Edit subscribed events** — change which events trigger this endpoint
* **Edit description** — update the human-readable label
* **Edit custom headers** — change static headers sent on every delivery
* **Disable** — stop firing without losing history (use the three-dot menu)
* **Delete** — remove the endpoint and its delivery history permanently

The **Success rate (24h)** column on the endpoints list shows the percentage of HTTP 2xx responses in the last 24 hours. A dash means no deliveries in that window.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Why isn't my endpoint receiving events?">
    Verify the endpoint is **Enabled**, the events you expect are in **Subscribed events**, and the URL is reachable from the public internet. Use **Send test event** to confirm end-to-end connectivity. If you want to confirm any traffic is flowing, subscribe to `terminal_command.logged` and `mcp_tool.logged` — the firehose events fire for every classified call regardless of risk or policy match. Then double-check the [Terminal Runs](https://gateway.getunbound.ai/ai-tools/terminal-runs) page for the activity you expected.
  </Accordion>

  <Accordion title="Signature verification fails on every event">
    The most common cause is verifying against a parsed JSON object instead of the raw body bytes. Make sure your handler reads the raw request body before parsing. Also confirm you copied the full `whsec_…` secret without truncation.
  </Accordion>

  <Accordion title="My receiver is slow / times out">
    Process the event asynchronously. Acknowledge with 200 immediately and queue the actual work — anything taking longer than 15 seconds will be treated as a failure and retried.
  </Accordion>

  <Accordion title="I want to replay an old event">
    Delivery history is visible per-endpoint, but events are not currently replayable through the UI. Re-trigger the source action in your AI tool, or contact support to manually replay a specific delivery.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Tool Policies" icon="shield" href="/policies/tool-policies">
    Configure which actions trigger which event types
  </Card>

  <Card title="Slack" icon="slack" href="/integrations/slack">
    Pair webhooks with Slack approval workflows
  </Card>
</CardGroup>
