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

# Webhooks introduction

> What webhooks are good for, what they must not be trusted with, and how they pair with reconciliation.

Webhooks exist because polling is a poor way to learn that something settled. They are not, however, a delivery guarantee — and the difference decides how you build.

## What they are good for

* **Latency.** A `fiat.withdrawal.completed` arrives seconds after settlement, where a poller might take minutes.
* **Fan-out.** One endpoint learns about transfers, cards, bills, accounts, and customers without seven pollers.
* **State you cannot ask for.** `account.updated` is how you learn `transferReady` flipped without watching for it.

## What they must not be trusted with

**Delivery is one attempt.** No retry, no backoff, no dead-letter redelivery. A deploy, a timeout, or a `502` from your load balancer loses the event, and the only recovery is a manual resend from the dashboard or your own reconciliation.

So:

| Decision | Safe on webhooks alone? |
| - | - |
| Refresh a dashboard | Yes |
| Send a customer a notification | Yes, if a missed one is survivable |
| Mark an invoice paid | **No** |
| Release goods or credit | **No** |
| Reconcile your ledger | **No** |

The rule that holds: **use the event to know *when* to look, and the REST resource to know *what is true*.**

```ts theme={null}
onEvent('fiat.withdrawal.completed', async (data) => {
  // The event says "look now". The resource says what happened.
  const transfer = await get(`/v1/transfers/${data.relatedOperation.id}`);
  await settle(transfer);
});
```

That pattern also solves ordering, because a resend can deliver an older event after a newer one. The resource is never stale.

## Pair every subscription with a sweeper

For anything that moves money, a scheduled reconciliation is not optional.

```ts theme={null}
// Every few minutes: anything that should have resolved by now.
const stale = await get('/v1/transfers?status=pending&sort=createdAt&pageSize=100');
for (const transfer of stale.items) {
  if (olderThan(transfer.createdAt, minutes(30))) await reconcile(transfer);
}
```

The webhook gives you speed. The sweeper gives you correctness. You need both, and the sweeper is the one you cannot skip.

## What you cannot configure

| | Available |
| - | - |
| Endpoint URL | Dashboard only |
| Enable or disable | Dashboard only |
| Rotate the signing secret | Dashboard only |
| Send a test event | Dashboard only |
| Read delivery history | Dashboard only |
| Resend a delivery | Dashboard only |
| **Choose which events you receive** | **Not available at all** |

<Warning>
  There is no webhook management endpoint on the Business API. An API-only integration cannot configure its own endpoint, rotate its secret, or resend a failed delivery — all six are dashboard actions. This is the fourth dashboard-only gap in the API, alongside account closure, transfer approval, and transfer policy.
</Warning>

## Environments are separate

Sandbox and live are separate deployments with separate endpoints and separate secrets. Configure each independently. An endpoint enabled in one does not exist in the other, and enabling one does not backfill events that occurred before it.

## Next

* [Get started](/webhooks/get-started) — a correct receiver in about thirty lines
* [Events](/webhooks/events) — the catalog and payload shapes
* [Setup and configuration](/webhooks/setup-and-configuration) — URLs, secrets, and what Sakawave will refuse


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.