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

# Webhooks

> Bankroll tells your server when a transaction carrying one of your managed references lands, over a signed delivery to a route the SDK provides.

Bankroll delivers webhooks to one route on your origin, `POST /api/bankroll/webhook`.
It carries one family of events: what became of the
[managed references](/build/payments#let-bankroll-watch-it) your server
minted, pay-ins and payouts alike. Deliveries are signed and retried until
your route answers 2xx. Bankroll never judges a transaction: it reports the
signature, and your server reads the transaction.

## Setup

Your exact origin must be [Bankroll Verified](/build/verified). Verification
creates your endpoint and hands you its secret with the signed manifest; set
it as `BANKROLL_WEBHOOK_SECRET` (builder apps have it already).

```ts theme={null}
// src/app/api/bankroll/webhook/route.ts
import { referenceWebhook } from "@joinbankroll/sdk/webhooks";

export const POST = referenceWebhook({ onConfirmed, onExpired });
```

`referenceWebhook` returns a plain `(request: Request) => Promise<Response>`
handler: a Next.js route handler as it stands, and usable anywhere else that
takes one. Pass `secret` to verify with something other than the environment
variable.

## Events

Bankroll delivers one event per reference.

| Event                 | When                                                                          | Fields beyond `reference` and `meta` |
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------ |
| `reference.confirmed` | The first successful transaction carrying the reference landed                | `signature`, `slot`                  |
| `reference.expired`   | The window ended with no successful transaction; Bankroll has stopped looking | `expiredAt`                          |

`meta` is what you gave `createManagedReference`, verbatim: it is how you
find the entry again. A failed transaction moves nothing and is not an event;
the reference stays open until something lands or the window ends.

Types are exported from `@joinbankroll/sdk/webhooks`:

```ts theme={null}
export interface ReferenceConfirmed {
  type: "reference.confirmed";
  reference: string;
  meta: Json;
  signature: string;
  slot: number;
}
export interface ReferenceExpired {
  type: "reference.expired";
  reference: string;
  meta: Json;
  expiredAt: string;
}
```

## What the route does

* Reads the raw body and verifies the signature against the secret. A
  delivery that does not verify, or carries no signature, is refused with 401
  before any handler runs; a body over 64 KiB with 413; an event the route
  cannot read with 400.
* Hands a `reference.confirmed` to `onConfirmed` and a `reference.expired` to
  `onExpired`, then answers 200.
* Lets a handler's throw propagate. The route answers 500 and Bankroll
  delivers again later, so never swallow a failure to record what you learned.

Handle each event idempotently: Bankroll retries until it gets a 2xx, so one
reference can be told about twice across a retry.

## Handling a confirmed reference

A reference is public once it lands, so anyone can attach it to a transaction
of their own. `reference.confirmed` is a candidate, not a receipt: always read
the transaction before releasing anything. For a pay-in,
[`checkCharge`](/build/payments#check-the-charge-against-the-sale) reads the charge and
checks it against what the entry was sold for. For a payout your server built
and sent itself, the signature the send answered is the proof: mark the
payout paid when Bankroll reports that one, and take Bankroll's word only
when the send's answer was lost.

```ts theme={null}
import { checkCharge, HSUSD_MINT } from "@joinbankroll/sdk/server";
import type { ReferenceConfirmed, ReferenceExpired } from "@joinbankroll/sdk/webhooks";

async function onConfirmed(event: ReferenceConfirmed) {
  const entry = await entries.find(event.meta.entryId);
  if (event.meta.side === "entry") {
    const charge = await checkCharge(event.signature, {
      payer: entry.player,
      payee: process.env.BANKROLL_PAYEE!,
      mint: HSUSD_MINT,
      amountCents: entry.priceCents,
      memo: entry.memo,
    });
    await entries.markPaid(entry.id, charge.signature);
  } else if (entry.sentSignature === null || entry.sentSignature === event.signature) {
    await entries.markPaidOut(entry.id, event.signature);
  }
}

async function onExpired(event: ReferenceExpired) {
  const entry = await entries.find(event.meta.entryId);
  if (event.meta.side === "entry") await entries.release(entry.id);
  else await entries.markUnpaid(entry.id); // never sent: build it again if still owed
}
```

`checkCharge` throws `ChargeMismatchError` with the `field` that differed;
treat a mismatch as not paid.

On `reference.expired`, release whatever waited on a pay-in, or treat the
payout as never sent. The window is yours to size when you mint the
reference: nothing lands after a charge's window plus its blockhash lifetime,
and a Privy-signed payout must not be rebuilt inside its 24 hour replay
window.

## Under the mock

With `BANKROLL_MOCK=1` outside production nothing reaches Bankroll. The
reference is minted locally, the mock host's `charge()` and a mock payout
deliver `reference.confirmed` to your route themselves, unsigned, and the
window's end delivers `reference.expired`. The route accepts an unsigned
delivery only under the mock. `npm run check` runs the whole loop.
