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

# Webhook Events

> Receive notifications when events happen in your Nickel account

Webhooks are the primary way that Nickel tells your application that something has happened. To receive the event notifications documented on this page, create a webhook subscription programmatically via the [`POST /webhook`](/api-reference/webhook/create-a-webhook) endpoint. Once you specify a webhook URL, Nickel sends an HTTP `POST` request to that URL whenever something happens against your Nickel account.

<Warning>
  Your account has a single API webhook subscription: creating a webhook replaces the API webhook already registered on your account, if any. A legacy payment-alert webhook configured from the dashboard settings page is separate and is left in place. To receive the events documented here, create your webhook via `POST /webhook`.
</Warning>

## Webhook structure

Every event is delivered as a JSON body with the same envelope:

```json theme={null}
{
  "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "created_at": "2026-01-31T23:59:59.000Z",
  "category": "disbursement.pending",
  "associated_object_type": "disbursement",
  "associated_object_id": "clrv6k8y80003wq02x36efjnv",
  "type": "event"
}
```

| Field                    | Description                                                                                                                       |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `id`                     | A unique identifier (UUID) for this event delivery.                                                                               |
| `created_at`             | When the event was created, as an ISO 8601 timestamp.                                                                             |
| `category`               | What happened — one of the categories listed below.                                                                               |
| `associated_object_type` | The kind of resource the event is about: `payment`, `disbursement`, `charge_authorization`, `bill_payment`, `check`, or `vendor`. |
| `associated_object_id`   | The ID of that resource. Use it to fetch the current state from the API.                                                          |
| `type`                   | Always the literal string `event`.                                                                                                |

## Delivery and retries

Nickel sends each event as a `POST` request with `Content-Type: application/json` and `Nickel-Signature` headers. Respond with any `2xx` status code to acknowledge receipt.

If your endpoint responds with a non-`2xx` status code or is unreachable, Nickel retries the delivery with exponential backoff (roughly 3s, 6s, 12s, and 24s between attempts) for up to 5 total attempts. Deliveries can occasionally arrive more than once or out of order, so make your handler idempotent — the event `id` is stable across retries of the same delivery. The signature is computed per attempt, so the `t` timestamp in the `Nickel-Signature` header reflects when that attempt was sent.

## Verifying signatures

The response to [`POST /webhook`](/api-reference/webhook/create-a-webhook) includes a `secret` — a `whsec_`-prefixed string that Nickel uses to sign every delivery to that webhook. Store it alongside your API key; you can also retrieve it later with [`GET /webhook`](/api-reference/webhook/get-webhooks).

Each delivery carries a `Nickel-Signature` header:

```
Nickel-Signature: t=1751850000,v1=4c47623a7e2f61be96a55eb60ea48a1dc105e3f38e0e78f4b788b6a2ac0f2c6b
```

* `t` is the unix timestamp (in seconds) of the delivery attempt.
* `v1` is the hex-encoded HMAC-SHA256 of the string `{t}.{raw request body}`, keyed with your webhook secret.

To verify a delivery:

1. Split the header into `t` and `v1`.
2. Compute HMAC-SHA256 over `{t}.{body}` with your secret, using the **raw request body bytes** — don't parse and re-serialize the JSON first, since any formatting difference changes the digest.
3. Compare your digest to `v1` with a constant-time comparison.
4. Reject deliveries whose `t` is too far from your current time (5 minutes is a reasonable tolerance) to guard against replayed requests.

```ts theme={null}
import { createHmac, timingSafeEqual } from 'crypto'

function verifyNickelSignature(
  header: string | undefined,
  rawBody: string,
  secret: string,
  toleranceSeconds = 300,
): boolean {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '')
  if (!match) return false

  const [, timestamp, signature] = match
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  return (
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - Number(timestamp)) <= toleranceSeconds
  )
}
```

Most web frameworks parse JSON bodies before your handler runs, so you'll need the raw body captured separately — for example `express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString() } })` in Express.

<Note>
  Webhooks created before signing shipped have no secret (`GET /webhook` returns `"secret": null`) and their deliveries are unsigned. Recreate the webhook with `POST /webhook` to get a secret — the new subscription replaces the old API webhook at the same or a new URL.
</Note>

As defense in depth, treat the webhook purely as a trigger: fetch the associated object from the API (see [Consuming Events](#consuming-events)) and act on that response rather than on the delivery payload itself.

## Categories

Nickel currently supports the following webhook notifications. Each entry says what changed and what your application should do about it — always by fetching the associated object, not by acting on the event alone.

### payment.made

A customer paid an invoice. Fetch the payment before recording anything: for a card payment it will show `SUCCEEDED`, but for a bank (ACH) payment this event fires when the debit is *initiated*, so the payment will still be `PENDING` — don't treat the money as final yet.

### payment.failed

A payment failed — most often the customer's bank returned an ACH debit, which can happen up to 60 days after `payment.made`. Fetch the payment and reverse whatever you recorded when it was made; the funds will not arrive (or will be clawed back if already paid out).

### payment.refunded

You refunded a settled payment. Fetch the payment and read `refunds[]` for the amount. A fully refunded payment reopens its invoice to `ACTIVE`, so the customer can pay again — delete the invoice if you don't want that.

### payment.voided

A refund was requested before the charge settled, so Nickel cancelled the original charge outright — no money ever moved and no processing fee applies. The fetched payment shows `refunds[].voided: true`.

### disbursement.pending

Nickel has batched settled payments into a disbursement headed for your bank account. Fetch the disbursement for the total and the `paymentIds` it contains — this is the moment to start reconciling against your bank statement.

### disbursement.succeeded

The receiving bank accepted the disbursement. Depending on your bank's turnaround it may not appear in your account balance yet — match it to the deposit when it does.

### charge\_authorization.authorized

Your customer authorized you to charge a payment method they put on file. Fetch the charge authorization and confirm `active: true` — you can now charge their open invoices against it.

### charge\_authorization.deleted

An authorization was revoked, by the customer or by you. Stop charging it: check `active` on the fetched authorization before every charge rather than caching yesterday's answer.

### bill\_payment.succeeded

Your vendor received the funds. The bill it paid now counts as `Paid` — safe to record in your books.

### bill\_payment.failed

A bill payment failed — for example, the debit from your funding source was returned. The bill is still owed; fix the funding source or delivery method and pay it again.

### bill\_payment.cancelled

A bill payment was cancelled before it went out to the payment network. Note that the fetched payment reports `status: "FAILED"` — this event is how you tell a cancellation apart from a genuine failure.

### check.sent

Nickel mailed the check to the vendor. Check events carry the **bill payment's** ID — fetch it with [`GET /billPayment/{billPaymentId}`](/api-reference/bill/get-a-bill-payment-by-id).

### check.delivered

The check arrived at the vendor's mailing address. Nothing to do — the vendor still needs to deposit it.

### check.deposited

The vendor deposited the check. The bill payment completes from here.

### check.returned

The check came back undeliverable. Confirm the vendor's mailing address (their check delivery method) and pay the bill again.

### check.failed

The check payment failed. Fetch the bill payment for its state, then retry with a corrected delivery method.

### vendor.payout\_method\_updated

A vendor's delivery method was created **or changed** — through the onboarding form or by you. Refetch the vendor and use the current `vendorDeliveryMethods[].id` when paying; a cached ID may now point at stale details.

## Consuming events

Individual events don't contain very much information on their own. This is by design, as the API structure can remain extremely stable and avoid difficult webhook migrations in the future as the Nickel API changes. If you need additional metadata, such as the amount of the disbursement in the envelope example above, make a `GET` request to the API for that information. You can use the `associated_object_type` or `category` fields to determine what resource to fetch from the API.

## See it in practice

<Columns cols={2}>
  <Card title="Receive webhooks" href="/guides/receive-webhooks" icon="bolt">
    Register an endpoint, build a reliable receiver, and test the loop in the sandbox.
  </Card>
</Columns>
