> For the complete documentation index, see [llms.txt](https://docs.brain.fi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.brain.fi/api-reference/webhooks-api.md).

# Webhooks

Inspect and replay failed deliveries on Brain's outbound webhook endpoints. This is the operator-facing surface for dead-lettered events. The inbound provider webhook (`POST /v1/raw/webhooks/{provider}`) lives in [Sources & Raw Ingestion](/api-reference/sources-api.md).

| Operation                 | Endpoint                                      |
| ------------------------- | --------------------------------------------- |
| List dead-letter events   | `GET /v1/webhooks/{endpoint_id}/dead-letters` |
| Replay dead-letter events | `POST /v1/webhooks/{endpoint_id}/replay`      |

Both routes are tenant-isolated. The `endpoint_id` belongs to the calling tenant; a cross-tenant id returns `404`.

### Event Types

Brain forwards a fixed allowlist of audit actions to registered endpoints. These are the only `event_type` values an outbound webhook carries:

| `event_type`                              | Fires when                                                    |
| ----------------------------------------- | ------------------------------------------------------------- |
| `agent.action.proposed`                   | An agent proposed an action                                   |
| `payment_intent.created`                  | A PaymentIntent is proposed                                   |
| `payment_intent.approved`                 | A required approval was recorded (or Policy said `allow`)     |
| `payment_intent.awaiting_second_approval` | A first approval landed and a distinct second approver is due |
| `proposal.awaiting_second_approval`       | Contract-named event for the awaiting-second-approval move    |
| `proposal.decided`                        | A surface proposal reached a terminal decision                |
| `payment_intent.rejected`                 | Policy or an approver rejected                                |
| `payment_intent.executed`                 | The intent was executed                                       |
| `payment_intent.failed`                   | Execution failed                                              |
| `payment_intent.reconciling`              | The intent was parked for reconciliation                      |
| `member.changed`                          | A member was created, changed, or deactivated                 |
| `payment_intent.execute.after`            | The §6 gate ran and the intent was dispatched to a rail       |
| `ledger.counterparty.created`             | A counterparty row was created                                |
| `ledger.counterparty.updated`             | A counterparty identity was edited                            |
| `ledger.transaction.created`              | A transaction row was created                                 |
| `ledger.obligation.created`               | An obligation row was created                                 |
| `policy.evaluate`                         | A policy decision was recorded                                |
| `raw.ingest.new`                          | A new Raw artifact was ingested                               |
| `raw.ingest.deduplicated`                 | A re-submitted artifact matched an existing one               |
| `raw.extraction.status_changed`           | A Raw extraction changed status                               |
| `raw.source.status_changed`               | A connected source changed status                             |

There is no `payment_intent.settled` event: rail settlement is async and confirmed via the rail-specific provider webhook plus the proof endpoint. `payment_intent.failed` **is** emitted when execution fails. The legacy bare `action.*` names are **not** emitted.

### How Dead-Lettering Works

Brain dispatches webhook deliveries asynchronously. Each row in the dead-letter table tracks an `attempt_count`; the delivery worker retries with exponential backoff up to **5 attempts**, after which the row is marked exhausted and stops auto-retrying. Replay (below) is the manual escape hatch.

### List Dead-Letter Events

```http
GET /v1/webhooks/{endpoint_id}/dead-letters
Authorization: Bearer <token>
```

```json
{
  "endpoint_id": "wh_ops_alerts",
  "dead_letters": [
    {
      "id": "dl_001",
      "event_id": "audit_evt_xyz",
      "event_type": "payment_intent.execute.after",
      "last_error": "503 Service Unavailable",
      "attempt_count": 5,
      "created_at": "2026-05-27T08:15:00Z",
      "last_attempt_at": "2026-05-27T08:47:12Z"
    }
  ]
}
```

### Replay Dead-Letter Events

Re-attempts delivery for every dead-letter row that is still under the attempt cap. Successes clear the row; failures bump `attempt_count`. The operation is **idempotent**. It accepts an `Idempotency-Key` header and is safe to retry.

```http
POST /v1/webhooks/{endpoint_id}/replay
Authorization: Bearer <token>
Idempotency-Key: <stable-key>
```

```json
{
  "endpoint_id": "wh_ops_alerts",
  "attempted": 7,
  "redelivered": 5,
  "still_failing": 2
}
```

If `still_failing > 0`, those rows had their `attempt_count` bumped; once a row hits 5 attempts it stops being auto-replayed and you can only retry it via this manual route after fixing the receiver.

### What's Next

<table data-view="cards"><thead><tr><th></th><th></th><th data-type="content-ref"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>📜 Audit API</strong></td><td>The events that drive outbound webhooks.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr><tr><td><strong>📥 Sources &#x26; Raw Ingestion</strong></td><td>The inbound webhook side (provider HMAC).</td><td><a href="/pages/EQ4MJytikUzDXJfFnfp5">/pages/EQ4MJytikUzDXJfFnfp5</a></td><td></td></tr></tbody></table>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.brain.fi/api-reference/webhooks-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
