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

# Webhooks

> Track settlement and status changes asynchronously.

Money movement is asynchronous. An off-ramp is requested, then converts, then settles
to a bank account over time. Webhooks let Spritz notify your backend the moment
something changes, so you don't have to poll.

Webhooks are configured per integrator, so you manage them with your integrator
credentials (HMAC). See [Authentication](/guides/authentication).

<Note>
  Webhooks are **global, not per user**. You subscribe once and receive events for all
  of your users. There's no need to register a webhook per user, and you don't pass a
  user API key when managing webhooks. These are integrator endpoints, so they use your
  integrator signing headers only.
</Note>

## Register an endpoint

Create a webhook by pointing it at an HTTPS URL on your backend and listing the events
you want. Use `"*"` to subscribe to every current and future event.

```bash theme={null}
curl -X POST https://platform.spritz.finance/v1/integrator/webhooks \
  -H "Content-Type: application/json" \
  # plus your integrator signing headers (see Authentication)
  -d '{
    "url": "https://api.example.com/webhooks/spritz",
    "events": ["onramp.created", "onramp.completed", "achDebitReturn.created"]
  }'
```

```json theme={null}
{
  "id": "6a43ac369288351e982157c4",
  "url": "https://api.example.com/webhooks/spritz",
  "events": ["onramp.created", "onramp.completed", "achDebitReturn.created"]
}
```

You can list, update, and delete webhooks too:

| Method   | Path                                  | Purpose                                |
| -------- | ------------------------------------- | -------------------------------------- |
| `GET`    | `/v1/integrator/webhooks`             | List your webhooks                     |
| `POST`   | `/v1/integrator/webhooks`             | Create a webhook                       |
| `PATCH`  | `/v1/integrator/webhooks/{webhookId}` | Update a webhook's events              |
| `DELETE` | `/v1/integrator/webhooks/{webhookId}` | Delete a webhook                       |
| `POST`   | `/v1/integrator/webhook-secret`       | Set the secret used to sign deliveries |

## Events

| Event                         | Fires when                                                        |
| ----------------------------- | ----------------------------------------------------------------- |
| `account.created`             | An account (bank account, card, bill) is created                  |
| `account.updated`             | An account's details change                                       |
| `account.deleted`             | An account is removed                                             |
| `payment.created`             | A payment (off-ramp) is initiated                                 |
| `payment.updated`             | A payment's details change                                        |
| `payment.completed`           | A payment completes                                               |
| `payment.refunded`            | A payment is refunded                                             |
| `verification.status.updated` | A user's identity verification status changes                     |
| `capabilities.updated`        | A user's capabilities change (for example on-ramp becomes active) |
| `onramp.created`              | An on-ramp record is created after a deposit is authorized        |
| `onramp.updated`              | An on-ramp's status, delivery, or reversal details change         |
| `onramp.completed`            | An on-ramp delivery completes                                     |
| `achDebitReturn.created`      | An ACH debit return is recorded                                   |
| `achDebitReturn.updated`      | An ACH debit return's details change                              |

Subscribe to `"*"` to receive all of these, including events added later.

## Receiving a delivery

Spritz sends a `POST` to your URL with a JSON body identifying what changed:

```json theme={null}
{
  "userId": "63d12d3b577fab6c6382136e",
  "id": "6368e3a3ec516e9572bbd23b",
  "event": "onramp.completed"
}
```

| Field    | Always present | Meaning                                 |
| -------- | -------------- | --------------------------------------- |
| `event`  | yes            | Which event fired, from the table above |
| `userId` | yes            | The user the event concerns             |
| `id`     | no             | The resource that changed               |

`id` is omitted for events whose subject is the user rather than a separate resource —
`capabilities.updated` is the case you are most likely to meet. Key your handler off
`event` and treat `id` as optional:

```ts theme={null}
const { event, userId, id } = payload

switch (event) {
  case "onramp.completed":
    await refreshOnRamp(id!)
    break
  case "capabilities.updated":
    // No `id` — the subject is the user.
    await refreshUser(userId)
    break
}
```

The body tells you which resource changed, not its full state. Treat it as a trigger:
fetch the resource from the API to get authoritative state before acting on it.

## Verifying signatures

Each delivery is signed so you can confirm it came from Spritz and wasn't modified in
transit. Set a webhook secret with `POST /v1/integrator/webhook-secret`, then verify
the `Signature` header against the raw request body using HMAC-SHA256. Always verify
against the raw body, before parsing JSON.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto"

function verifySpritzWebhook(rawBody: string, signature: string, secret: string) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex")
  const a = Buffer.from(expected, "utf8")
  const b = Buffer.from(signature, "utf8")
  return a.length === b.length && timingSafeEqual(a, b)
}

// In your handler, using the raw (unparsed) body:
const signature = request.headers["signature"]
if (!signature || !verifySpritzWebhook(rawBody, signature, WEBHOOK_SECRET)) {
  throw new Error("Invalid webhook signature")
}
```

## Best practices

<AccordionGroup>
  <Accordion title="Respond fast, process later">
    Acknowledge with a `2xx` immediately and hand off to a queue. Slow responses can
    trigger retries and duplicate processing.
  </Accordion>

  <Accordion title="Make handlers idempotent">
    Retries mean the same event can arrive more than once. Deduplicate on the resource
    `id`. See [Idempotency](/guides/idempotency).
  </Accordion>

  <Accordion title="Don't rely on event order">
    Events are dispatched concurrently and are **not ordered**. `onramp.completed` can
    arrive before `onramp.created` for the same on-ramp, and does so most often when the
    two transitions happen close together.

    The payload carries no timestamp, so order cannot be reconstructed from the event
    alone. Key your handler off the resource `id`, fetch current state from the API, and
    make each handler safe to run regardless of what has already been processed for that
    resource — including the case where the first event you ever see for a resource is
    its last one.
  </Accordion>

  <Accordion title="Treat events as triggers, not truth">
    On receipt, fetch the current resource from the API to get authoritative state
    rather than relying solely on the event body.
  </Accordion>
</AccordionGroup>
