# Webhooks

FervusAI sends webhook events to your server when state changes occur across your agent fleet. Events are delivered as HTTP POST requests with a JSON body.

***

## Registering a webhook

```bash
curl -X POST https://api.fervus.ai/v1/webhooks \
  -H "Authorization: Bearer $FERVUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/fervus-events",
    "events": [
      "transaction.completed",
      "transaction.blocked",
      "balance.low"
    ]
  }'
```

Subscribe to `"*"` to receive all events.

**Response `200 OK`**

```json
{
  "webhook_id": "wh_7b3f1e",
  "url": "https://your-server.com/fervus-events",
  "events": ["transaction.completed", "transaction.blocked", "balance.low"],
  "signing_secret": "whsec_...",
  "created_at": "2026-04-29T12:00:00Z"
}
```

Store the `signing_secret` - you will use it to verify incoming webhook signatures.

***

## Verifying signatures

Every webhook request includes a `Fervus-Signature` header. Verify it to ensure the request originated from FervusAI and has not been tampered with.

The signature is a HMAC-SHA256 of the raw request body using your webhook signing secret.

```typescript
import crypto from "crypto";

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express example
app.post("/fervus-events", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["fervus-signature"] as string;
  const isValid = verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!);
  
  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  // Handle event...
  res.status(200).send("OK");
});
```

Always use `crypto.timingSafeEqual` for signature comparison to prevent timing attacks.

***

## Responding to webhooks

Return a `2xx` status code within 5 seconds to acknowledge receipt. FervusAI considers any non-2xx response or a timeout as a failed delivery and will retry.

**Retry schedule:**

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |

After 5 failed attempts, the webhook is marked `degraded` and delivery is paused. Resolve the issue and re-enable it in the dashboard under **Settings → Webhooks**.

Do not perform long-running processing synchronously in your webhook handler. Acknowledge immediately and enqueue the event for async processing.

***

## Event catalogue

### Transaction events

**`transaction.completed`**

```json
{
  "event": "transaction.completed",
  "wallet_id": "wal_9f3a2b",
  "tx_id": "tx_4f7c1a",
  "tx_signature": "5KtP...xyz",
  "amount": "2.50",
  "recipient": "api.perplexity.ai",
  "policy_triggered": false,
  "on_chain_slot": 321847293,
  "timestamp": "2026-04-29T12:00:00Z"
}
```

**`transaction.blocked`**

```json
{
  "event": "transaction.blocked",
  "wallet_id": "wal_9f3a2b",
  "attempted_amount": "12.00",
  "recipient": "api.perplexity.ai",
  "violation": {
    "rule": "max_per_tx",
    "limit": "5.00",
    "attempted": "12.00"
  },
  "timestamp": "2026-04-29T12:00:00Z"
}
```

**`transaction.pending`**

Fired when a transaction is submitted to Solana but not yet confirmed.

### Wallet events

**`wallet.created`**

```json
{
  "event": "wallet.created",
  "wallet_id": "wal_9f3a2b",
  "agent_id": "agent_research_v2",
  "solana_address": "7xKp...nR4Q",
  "timestamp": "2026-04-29T12:00:00Z"
}
```

**`wallet.funded`** - USDC allocated to the wallet.

**`wallet.frozen`** - Wallet manually frozen by an operator.

**`wallet.unfrozen`** - Wallet re-activated.

### Balance events

**`balance.low`**

```json
{
  "event": "balance.low",
  "wallet_id": "wal_9f3a2b",
  "current_balance": "8.50",
  "threshold": "10.00",
  "timestamp": "2026-04-29T12:00:00Z"
}
```

### Policy events

**`policy.updated`** - A policy was modified. Includes a diff of changed fields.

**`policy.expired`** - A policy with an expiry timestamp has expired. Attached wallets are frozen.

### Escrow events

**`escrow.created`** - A2A escrow PDA created, funds locked.

**`escrow.released`** - Completion proof verified, funds released to receiving agent.

**`escrow.refunded`** - Timeout or dispute resolved in favour of sender; funds returned.

**`escrow.disputed`** - Dispute raised on an active escrow.

### Organisation events

**`org.deposit_confirmed`** - Organisation USDC deposit confirmed on-chain.

**`export.ready`** - A requested transaction export is ready for download.

***

## Managing webhooks

**List webhooks:** `GET /v1/webhooks`

**Get a webhook:** `GET /v1/webhooks/{webhook_id}`

**Update a webhook:** `PATCH /v1/webhooks/{webhook_id}` - update URL or event subscriptions.

**Delete a webhook:** `DELETE /v1/webhooks/{webhook_id}`

**Test a webhook:** `POST /v1/webhooks/{webhook_id}/test` - sends a synthetic `ping` event to your endpoint.


---

# Agent Instructions: 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:

```
GET https://docs.fervusai.com/api-reference/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
