---
title: Webhook notifier
description: Generic HTTP POST. Retries, exponential backoff, custom headers.
---

<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", margin: "0.5rem 0 1.25rem" }}>
	<Logo name="webhook" size={28} />
	<span style={{ fontSize: "0.8125rem", color: "var(--color-fd-muted-foreground)" }}>
		Slot: <code>notifier</code> · Name: <code>webhook</code>
	</span>
</div>

<PlatformSupport macos="full" linux="full" windows="full" />

Point it at any HTTPS endpoint. AO POSTs a JSON event. Use this for PagerDuty-style integrations, custom dashboards, or anything we don't have a dedicated notifier for.

## Setup

Run the setup command:

```bash
ao setup webhook
```

If a webhook notifier is already configured, the command asks whether to use the
existing webhook URL, add a new webhook URL, or cancel. The add-new path lets you
paste a different endpoint, go back, or cancel before AO sends the setup test.
Before saving, AO also asks which notification priorities the webhook should
receive: `urgent-only`, `urgent-action`, or `all`. For scriptable setup, pass
`--routing-preset <preset>`.

## Use

```yaml title="agent-orchestrator.yaml"
notifiers:
  webhook:
    plugin: webhook
    url: https://example.com/ao-events
    headers:
      Authorization: "Bearer ${MY_TOKEN}"
      X-Source: "agent-orchestrator"
    retries: 5 # default 3
    retryDelayMs: 2000 # default 1000

notificationRouting:
  urgent: [webhook]
  action: [webhook]
```

## Config

| Key            | Required | Default | What it does                                                 |
| -------------- | -------- | ------- | ------------------------------------------------------------ |
| `url`          | ✓        | —       | HTTPS endpoint that accepts POST                             |
| `headers`      | optional | `{}`    | Custom request headers (supports `${ENV_VAR}` interpolation) |
| `retries`      | optional | `3`     | Retry count on 429/5xx/network errors                        |
| `retryDelayMs` | optional | `1000`  | Base retry delay (exponential backoff)                       |

## Payload

### Envelope

Every POST has a `type` discriminant. Most events use `notification`:

```json
{
	"type": "notification",
	"event": {
		"id": "3f6a1b2c-...",
		"type": "pr.created",
		"priority": "info",
		"sessionId": "sess_01HXY...",
		"projectId": "myproject",
		"timestamp": "2026-04-17T15:00:00.000Z",
		"message": "PR opened: fix(auth): handle token expiry",
		"data": {}
	}
}
```

When actions are attached (e.g. approve/retry buttons in supported notifiers):

```json
{
	"type": "notification_with_actions",
	"event": { "...": "same fields as above" },
	"actions": [{ "label": "View PR", "url": "https://github.com/owner/repo/pull/123" }]
}
```

Free-form messages sent via `ao send` arrive as:

```json
{
	"type": "message",
	"message": "Hello from AO",
	"context": {}
}
```

### Event types

| `event.type`                 | Default priority | When emitted                                      |
| ---------------------------- | ---------------- | ------------------------------------------------- |
| `session.spawned`            | `info`           | Session created                                   |
| `session.working`            | `info`           | Agent begins working on the issue                 |
| `session.exited`             | `info`           | Agent process exited cleanly                      |
| `session.killed`             | `info`           | Session was manually killed                       |
| `session.idle`               | `info`           | Agent has been idle for a while                   |
| `session.stuck`              | `urgent`         | Agent appears stuck (no activity)                 |
| `session.needs_input`        | `urgent`         | Agent is blocked waiting for user input           |
| `session.errored`            | `urgent`         | Agent hit an unrecoverable error                  |
| `pr.created`                 | `info`           | PR opened by the agent                            |
| `pr.updated`                 | `info`           | PR updated (new commits pushed)                   |
| `pr.merged`                  | `action`         | PR was merged                                     |
| `pr.closed`                  | `info`           | PR was closed without merging                     |
| `ci.passing`                 | `info`           | CI checks passed                                  |
| `ci.failing`                 | `warning`        | CI checks failed                                  |
| `ci.fix_sent`                | `info`           | Agent sent a CI fix to itself                     |
| `ci.fix_failed`              | `warning`        | Agent's CI fix attempt failed                     |
| `review.pending`             | `info`           | PR review requested                               |
| `review.approved`            | `action`         | PR approved by reviewer                           |
| `review.changes_requested`   | `warning`        | Reviewer requested changes                        |
| `review.comments_sent`       | `info`           | Review comments sent to agent                     |
| `review.comments_unresolved` | `info`           | Agent left unresolved review comments             |
| `automated_review.found`     | `warning`        | Automated review tool (e.g. BugBot) left comments |
| `automated_review.fix_sent`  | `info`           | Agent sent fixes for automated review comments    |
| `merge.ready`                | `action`         | PR is approved + CI green — ready to merge        |
| `merge.conflicts`            | `warning`        | Merge conflicts detected                          |
| `merge.completed`            | `action`         | PR was successfully merged                        |
| `reaction.triggered`         | `info`           | A configured reaction was triggered               |
| `reaction.escalated`         | `urgent`         | A reaction was escalated to a human notifier      |
| `summary.all_complete`       | `info`           | All sessions in the project completed             |

### Signature verification

The webhook notifier does **not** sign outgoing requests. There is no `X-Signature` or HMAC header added to POSTs. To secure your endpoint:

- Use a secret `Authorization` header (set via `headers.Authorization` in config, passed as `${MY_TOKEN}`).
- Restrict inbound traffic to your AO host via IP allowlist or VPN.
- Use a reverse proxy with mutual TLS if needed.

## Why use this over a specific notifier

- You want a single endpoint regardless of Slack/Discord/email routing (handled downstream).
- You're building an internal AO dashboard of your own.
- You want structured events for observability tooling (Datadog, Honeycomb, etc.).
