# Webhooks

Aurea sends webhooks for Agent Payments, the bank ramp and the card onramp. For wallets, transactions and swaps, read the state through the API.

## Overview

Two different things are called webhooks in these docs:

- **Outbound — Aurea calls your server.** For Agent Payments events, which this page describes, and for the changes to your users' bank ramp KYC, deposits, transactions and payouts and to their card onramp purchases, described in [Events to Your Server](https://docs.aureahub.com/docs/guide-events.md). The two are registered, signed and retried differently.
- **Inbound — the bank ramp reports to Aurea.** Its events update KYC status, deposits and payouts inside Aurea. Aurea receives them once the bank ramp is connected for your tenant; there is nothing to subscribe on your side ([Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md)).

Aurea doesn't push wallet, transaction, swap or EURe changes to your servers.

## Agent Payment Events

| type | When | data |
| --- | --- | --- |
| agent.payment.settled | An agent payment was executed on its rail | `{ executionId, agentId, amount, currency, rail }` |
| agent.payment.denied | The spend policy denied the payment | `{ executionId, agentId, reason }` |
| agent.payment.rejected | A payment waiting for approval was rejected | `{ executionId, agentId, reason }` |

## Registering an Endpoint

With a tenant admin access token, call `POST /v1/agent-payments/webhooks/endpoints` (201 with the created endpoint). `GET /v1/agent-payments/webhooks/endpoints` lists them under `data`.

| Field | Description |
| --- | --- |
| url | Required. `http` or `https`; no credentials in the URL. `localhost` and hosts that resolve to private, loopback, link-local or cloud-metadata addresses are refused, also after redirects. |
| events | Optional list of event types. Omitted or empty means all events. |
| secret | Optional, 8–255 characters. Without a secret, deliveries are not signed. |
| agentId | Optional agent ID. |

```javascript
await fetch('https://api.aureahub.com/v1/agent-payments/webhooks/endpoints', {
  method: 'POST',
  headers: { Authorization: `Bearer ${tenantAdminToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com/aurea/webhooks',
    events: ['agent.payment.settled', 'agent.payment.denied'],
    secret: process.env.AUREA_WEBHOOK_SECRET
  })
});
```

## Deliveries

Each delivery is an HTTP `POST` with a JSON body:

```json
{
  "id": "<execution ID>:<endpoint ID>",
  "type": "agent.payment.settled",
  "data": { "executionId": "…", "agentId": "…", "amount": "…", "currency": "…", "rail": "…" }
}
```

| Header | Value |
| --- | --- |
| Content-Type | application/json |
| X-Aurea-Event | The event type |
| X-Aurea-Delivery | ID of this delivery |
| X-Aurea-Signature | `sha256=<hex>` — only when the endpoint has a secret |

- An event is queued at most once per endpoint, and its `id` is stable across retries: use it to skip events you have already processed.
- There is no timestamp header, so deduplicating on `id` is also your replay protection.
- Queued deliveries are sent when the delivery worker runs. A tenant admin can run it with `POST /v1/agent-payments/webhooks/run`, which returns `{ delivered, retried, dead }`; whether it also runs on a schedule depends on the deployment.

## Verifying the Signature

`X-Aurea-Signature` is `sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with the endpoint's secret. Verify it on the exact bytes you received, before parsing the JSON.

```typescript
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

export function verifyAureaWebhook(rawBody: Buffer, signature: string | undefined, secret: string): boolean {
  if (!signature) return false;
  const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'));
  const received = Buffer.from(signature);
  return expected.length === received.length && timingSafeEqual(expected, received);
}

const app = express();
app.post('/aurea/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifyAureaWebhook(req.body, req.get('x-aurea-signature'), process.env.AUREA_WEBHOOK_SECRET!)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString('utf8'));
  if (!(await alreadyProcessed(event.id))) await handleEvent(event);
  res.sendStatus(200);
});
```

## Retries

Any `2xx` answer marks the delivery as delivered. Any other status, or a network error, schedules a retry after 1, 2, 4, 8 and 16 minutes. After the sixth failed attempt the delivery is marked dead and not retried. Retries are sent when the delivery worker runs after they become due.

## Everything Else

- **Transactions and swaps:** poll [Get Tx Status](https://docs.aureahub.com/docs/tx-status.md) or the [Unified Feed](https://docs.aureahub.com/docs/agg-tx-list.md).
- **The bank ramp:** [Events to Your Server](https://docs.aureahub.com/docs/guide-events.md), or read [Onboarding Status](https://docs.aureahub.com/docs/payin-status.md), [Get Deposits](https://docs.aureahub.com/docs/payin-deposits.md) and [Get Payout Transaction](https://docs.aureahub.com/docs/payout-get.md).
- **Users' devices:** Aurea sends push notifications for some events, such as completed the bank ramp deposits and payouts, to devices registered with [Register Device](https://docs.aureahub.com/docs/notif-register.md).

---

Web version: https://docs.aureahub.com/#webhooks
