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

# Webhooks

> Signed HTTPS notifications for ~50 event types, with plan-tier retry policies.

Webhooks are how Ovra tells your systems that something happened. Every state-changing operation fires one. Deliveries are HMAC-SHA256-signed, retried on a plan-tier-aware backoff schedule, and SSRF-checked against private IPs and metadata endpoints.

## Subscribe

```bash theme={}
curl -X POST https://api.getovra.com/webhooks \
  -H "Authorization: Bearer $OVRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/ovra/webhook",
    "events": ["transaction.completed", "intent.approved", "claim.request.paid"],
    "description": "Production webhook"
  }'
```

Use `["*"]` to subscribe to every event.

URL constraints (`isBlockedWebhookUrl`):

* HTTPS or HTTP (HTTPS strongly recommended in prod)
* No localhost
* No RFC 1918 (10.0.0.0/8, 172.16/12, 192.168/16)
* No link-local (169.254.0.0/16) or metadata endpoints (`169.254.169.254`, `metadata.google.internal`)
* DNS rebinding check at delivery time

## Event catalog

There are \~50 event types across these categories. Source of truth: `apps/api/src/services/webhook.ts` `WebhookEvent` union.

<AccordionGroup>
  <Accordion title="Intent">
    `intent.created` · `intent.approved` · `intent.denied` · `intent.expired` · `intent.cancelled` · `intent.matched` (reserved) · `intent.mismatched` (reserved)
  </Accordion>

  <Accordion title="Transaction">
    `transaction.created` · `transaction.updated` · `transaction.authorization` · `transaction.settlement` · `transaction.settled` · `transaction.completed` · `transaction.declined` · `transaction.refunded`
  </Accordion>

  <Accordion title="Card">
    `card.issued` · `card.activated` · `card.frozen` · `card.unfrozen` · `card.closed` · `card.rotated` · `card.funded` · `card.shipped` · `card.limits_changed` · `card.details_changed`
  </Accordion>

  <Accordion title="Card limit request">
    `card_limit_request.updated`
  </Accordion>

  <Accordion title="Wallet">
    `wallet.created` · `wallet.funded` · `wallet.credited`
  </Accordion>

  <Accordion title="Transfer">
    `transfer.completed` · `transfer.failed` · `transfer.split.completed`
  </Accordion>

  <Accordion title="Risk">
    `risk.agent_frozen` · `risk.review_required` · `risk.alert_created` · `risk.denied` · `risk.velocity_alert` · `risk.geo_impossible`
  </Accordion>

  <Accordion title="Agent">
    `agent.created` · `agent.frozen` · `agent.unfrozen`
  </Accordion>

  <Accordion title="Collect (legacy `claim.*` namespace)">
    `claim.request.created` · `claim.request.paid` · `claim.request.expired` · `claim.request.cancelled`
  </Accordion>

  <Accordion title="Dispute">
    `dispute.created` · `dispute.updated` · `dispute.resolved`
  </Accordion>

  <Accordion title="MPP (credential issuer)">
    `mpp.credential.minted` · `mpp.credential.consumed` · `mpp.credential.expired` (reserved) · `mpp.transaction.completed`
  </Accordion>

  <Accordion title="Verification + misc">
    `verification.mismatch` · `mobile_wallet.updated` · `payment.updated` · `receipt.updated` · `bill.updated` · `cardholder.updated`
  </Accordion>
</AccordionGroup>

## Delivery format

Headers:

| Header               | Value                                                              |
| -------------------- | ------------------------------------------------------------------ |
| `X-Ovra-Event`       | Event name                                                         |
| `X-Ovra-Timestamp`   | Unix seconds at signing                                            |
| `X-Ovra-Delivery-Id` | Unique per attempt                                                 |
| `X-Ovra-Signature`   | `sha256=<hex>` HMAC-SHA256 of `{deliveryId}.{timestamp}.{rawBody}` |
| `X-Request-Id`       | Correlation back to the originating request                        |

Body:

```json theme={}
{
  "event": "transaction.completed",
  "createdAt": "2026-04-20T12:34:56.789Z",
  "data": {
    "transactionId": "tx_...",
    "agentId": "ag_...",
    "intentId": "int_...",
    "amountEuros": 4.51,
    "merchant": "hetzner.com",
    "status": "completed"
  }
}
```

## Verify the signature

<CodeGroup>
  ```ts Node theme={}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verify(req: { headers: Record<string,string>, rawBody: Buffer }, secret: string) {
    const sig = req.headers["x-ovra-signature"] ?? "";
    const ts  = req.headers["x-ovra-timestamp"] ?? "";
    const id  = req.headers["x-ovra-delivery-id"] ?? "";
    const expected = "sha256=" + createHmac("sha256", secret)
      .update(`${id}.${ts}.${req.rawBody.toString("utf8")}`)
      .digest("hex");
    return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  }
  ```

  ```python Python theme={}
  import hmac, hashlib
  def verify(headers, raw_body, secret):
      sig = headers.get("x-ovra-signature", "")
      ts  = headers.get("x-ovra-timestamp", "")
      did = headers.get("x-ovra-delivery-id", "")
      expected = "sha256=" + hmac.new(secret.encode(),
          f"{did}.{ts}.{raw_body.decode()}".encode(),
          hashlib.sha256).hexdigest()
      return hmac.compare_digest(sig, expected)
  ```
</CodeGroup>

## Retry policy (plan-tier-aware)

| Tier         | Max attempts | Backoff                 | Dead-letter |
| ------------ | ------------ | ----------------------- | ----------- |
| `basic`      | 1            | –                       | –           |
| `full`       | 1            | –                       | –           |
| `full_retry` | 5            | 1m · 5m · 15m · 1h · 4h | –           |
| `full_dlq`   | 5            | Same as above           | ✓           |

Background poller runs every 30s, claims rows via `FOR UPDATE SKIP LOCKED` so multi-instance API hosts never double-deliver.

| Plan       | Webhook tier |
| ---------- | ------------ |
| Free       | basic        |
| Starter    | full         |
| Business   | full\_retry  |
| Enterprise | full\_dlq    |

## Best practices

* **Verify signatures** before processing — never trust the body unsigned.
* **Idempotent handlers** — the same event may deliver more than once.
* **Acknowledge fast** — return 2xx within 30s. Defer slow work to a queue.
* **Check the timestamp window** to reject very old replays.

## Next

<CardGroup cols={2}>
  <Card title="Intelligence" icon="chart-line" href="/concepts/intelligence">
    Audit + decision logs supplement webhooks for forensics.
  </Card>

  <Card title="Sandbox" icon="flask" href="/concepts/sandbox">
    Test signature verification end-to-end.
  </Card>
</CardGroup>
