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

# Webhooks

> Configure webhook endpoints to receive real-time notifications for agent events.

# Webhooks

Steward delivers real-time event notifications to your configured webhook endpoints. Every transaction state change, policy violation, and spend threshold crossing fires a signed HTTP POST to your URL.

**Base path:** `/webhooks`\
**Auth:** Tenant owner/admin session with recent MFA verification

***

## Event Types

| Event              | Fired when                                         |
| ------------------ | -------------------------------------------------- |
| `tx.pending`       | A transaction is queued for human approval         |
| `tx.approved`      | A pending transaction is approved                  |
| `tx.denied`        | A pending transaction is denied                    |
| `tx.signed`        | A transaction is signed (and optionally broadcast) |
| `spend.threshold`  | An agent's spend tracking threshold is crossed     |
| `policy.violation` | A hard policy rejects a transaction                |

You can subscribe to any subset of events or omit `events` to receive all of them.

***

## Register a Webhook

```bash theme={null}
POST /webhooks
```

```bash theme={null}
curl -X POST https://api.steward.fi/webhooks \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/steward",
    "events": ["tx.pending", "tx.signed", "policy.violation"],
    "description": "Main webhook for agent notifications",
    "maxRetries": 5,
    "retryBackoffMs": 60000
  }'
```

**Response:**

```json theme={null}
{
  "ok": true,
  "data": {
    "id": "wh_550e8400-...",
    "tenantId": "your-tenant",
    "url": "https://your-app.com/webhooks/steward",
    "events": ["tx.pending", "tx.signed", "policy.violation"],
    "secret": "whsec_a1b2c3d4...",
    "enabled": true,
    "maxRetries": 5,
    "retryBackoffMs": 60000,
    "description": "Main webhook for agent notifications",
    "createdAt": "2026-03-27T00:00:00Z"
  }
}
```

<Warning>
  The `secret` is only returned on creation. Store it securely — you'll need it to verify incoming webhook signatures.
</Warning>

**Body fields:**

| Field            | Required | Description                                              |
| ---------------- | -------- | -------------------------------------------------------- |
| `url`            | ✅        | HTTPS endpoint to receive events                         |
| `events`         | —        | Array of event types to subscribe to (default: all)      |
| `description`    | —        | Human-readable label                                     |
| `maxRetries`     | —        | Max delivery attempts on failure (0–10, default: 5)      |
| `retryBackoffMs` | —        | Milliseconds between retries (min: 1000, default: 60000) |

***

## List Webhooks

```bash theme={null}
GET /webhooks
```

```bash theme={null}
curl https://api.steward.fi/webhooks \
  -H "X-Steward-Key: your-tenant-key"
```

Returns all webhooks for the tenant. The `secret` field is omitted from list responses.

***

## Update a Webhook

```bash theme={null}
PUT /webhooks/:id
```

```bash theme={null}
curl -X PUT https://api.steward.fi/webhooks/wh_550e8400-... \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false,
    "events": ["tx.pending", "tx.approved", "tx.denied"]
  }'
```

Updatable fields: `url`, `events`, `enabled`, `description`, `maxRetries`, `retryBackoffMs`.

***

## Delete a Webhook

```bash theme={null}
DELETE /webhooks/:id
```

```bash theme={null}
curl -X DELETE https://api.steward.fi/webhooks/wh_550e8400-... \
  -H "X-Steward-Key: your-tenant-key"
```

***

## Delivery History

```bash theme={null}
GET /webhooks/:id/deliveries?limit=50&offset=0&status=failed&eventType=tx.pending&hasError=true
```

Returns recent delivery attempts with redacted status metadata. Payloads,
webhook URLs, secrets, and raw error text are intentionally omitted from this
list response. Optional filters include `status`, `eventType`, and `hasError`.

```json theme={null}
{
  "ok": true,
  "data": [
    {
      "id": "del_...",
      "eventType": "tx.signed",
      "status": "delivered",
      "attempts": 1,
      "maxAttempts": 6,
      "nextRetryAt": null,
      "hasError": false,
      "createdAt": "2026-03-27T12:34:56Z",
      "deliveredAt": "2026-03-27T12:34:57Z"
    },
    {
      "id": "del_...",
      "eventType": "tx.pending",
      "replayedFromDeliveryId": "del_original_...",
      "status": "failed",
      "attempts": 5,
      "maxAttempts": 6,
      "nextRetryAt": "2026-03-27T12:35:56Z",
      "hasError": true,
      "createdAt": "2026-03-27T12:34:56Z",
      "deliveredAt": null
    }
  ]
}
```

## Export Delivery History

```bash theme={null}
GET /webhooks/:id/deliveries/export?limit=1000&status=failed&eventType=tx.pending&hasError=true
```

Downloads the same redacted delivery-history fields as CSV for offline incident
review. The export is bounded to 10,000 rows, supports the same filters as the
JSON history endpoint, omits payloads, webhook URLs, secrets, and raw error
text, and escapes spreadsheet formula prefixes in every cell.

## Retry a Failed Delivery

```bash theme={null}
POST /webhooks/deliveries/:id/retry
```

Queues an eligible failed delivery for immediate retry without resetting its
attempt count or bypassing the configured retry budget.

## Replay a Historical Delivery

```bash theme={null}
POST /webhooks/deliveries/:id/replay
```

Creates and sends a new signed delivery from a historical delivery payload.
Replay is separate from retry: retry reschedules the original failed delivery
and preserves its attempt budget, while replay creates a new delivery ID and
signature so receivers can distinguish it from the original. Replay requires an
owner/admin session with recent MFA, and Steward only replays if the original
webhook still exists, is enabled, has the same URL, and is still subscribed to
the event type. Pending or processing deliveries cannot be replayed.

The response is the new redacted delivery. Replayed deliveries include
`replayedFromDeliveryId`.

## Send a Diagnostic Test Delivery

```bash theme={null}
POST /webhooks/:id/test
```

Sends a one-off signed `webhook.test` delivery to an enabled endpoint. Test
deliveries are diagnostic only; `webhook.test` is not a subscribable event type
and is not added to endpoint event filters.

***

## Payload Format

All events share the same envelope:

```json theme={null}
{
  "id": "evt_abc123",
  "event": "tx.signed",
  "tenantId": "your-tenant",
  "agentId": "agent-1",
  "timestamp": "2026-03-27T12:34:56.789Z",
  "data": { ... }
}
```

**Event-specific `data` fields:**

<Accordion title="tx.pending">
  ```json theme={null}
  {
    "approvalId": "appr_...",
    "txId": "tx_...",
    "to": "0xSomeAddress",
    "value": "500000000000000000",
    "chainId": 8453,
    "policyResults": [
      { "policyId": "auto-threshold", "type": "auto-approve-threshold", "passed": false, "reason": "Value exceeds threshold" }
    ]
  }
  ```
</Accordion>

<Accordion title="tx.signed">
  ```json theme={null}
  {
    "txId": "tx_...",
    "txHash": "0xabc...",
    "to": "0xSomeAddress",
    "value": "10000000000000000",
    "chainId": 8453,
    "broadcast": true
  }
  ```
</Accordion>

<Accordion title="policy.violation">
  ```json theme={null}
  {
    "txId": "tx_...",
    "to": "0xBlockedAddress",
    "value": "1000000000000000000",
    "failedPolicies": [
      { "policyId": "whitelist", "type": "approved-addresses", "passed": false, "reason": "Address not in whitelist" }
    ]
  }
  ```
</Accordion>

***

## Verifying Signatures

Every delivery includes an `X-Steward-Signature` header. Verify it to ensure the payload came from Steward:

```typescript theme={null}
import { createHmac } from "crypto";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return `sha256=${expected}` === signature;
}

// Express example
app.post("/webhooks/steward", (req, res) => {
  const sig = req.headers["x-steward-signature"] as string;
  const body = JSON.stringify(req.body);

  if (!verifyWebhookSignature(body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  const { event, agentId, data } = req.body;

  if (event === "tx.pending") {
    // Notify your team to review the pending transaction
    await notifyAdmin(agentId, data.approvalId, data.value);
  }

  res.json({ ok: true });
});
```

***

## Delivery Guarantees

* Steward delivers with **at-least-once** semantics — design your handler to be idempotent
* Failed deliveries are retried up to `maxRetries` times with `retryBackoffMs` spacing
* Your endpoint should respond `2xx` within 10 seconds or the attempt is counted as failed
* Delivery history is kept for 30 days

***

## Related

* [Approvals](/api-reference/approvals) — Review and act on `tx.pending` events
* [Tenant Config](/api-reference/tenant-config) — Enable `webhookCallbackEnabled` in approval config
