> ## 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.

# Timers

> Set a timer on Bankroll; when it fires, Bankroll calls your server with the meta you gave it.

A timer lives on Bankroll instead of in your process. Set one with a `meta`
and a number of minutes, and when it fires Bankroll delivers `timer.fired`
with that `meta` to your [webhook route](/build/webhooks), once. It is for
what your server must do when nobody is asking it: a no-show deadline, a
round that ends on the clock. Your server keeps no timer and runs no cron.

## Setup

Your exact origin must be [Bankroll Verified](/build/verified), and your
webhook route must be in place: a fired timer is delivered the same way as a
reference event, signed with `BANKROLL_WEBHOOK_SECRET`.

## Set a timer

`createTimer` is on `@joinbankroll/sdk/server` and takes the same
`{ origin }` as `createManagedReference`.

```ts theme={null}
import { createTimer } from "@joinbankroll/sdk/server";

const { id, at } = await createTimer(
  { meta: { kind: "deadline", roundId: round.id }, firesInMinutes: 15 },
  { origin: "https://acme.example" },
);
await rounds.setDeadline(round.id, { timerId: id, at });
```

| Field            | Notes                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `meta`           | Your own routing data, plain JSON up to 4 KiB, echoed verbatim on the event. Put in it what you need to find the subject again. |
| `firesInMinutes` | Whole minutes from now, 1 to 43200 (thirty days). Bankroll fires timers once a minute, so a timer is set to the minute.         |
| `id`             | The timer's id, on the event as well.                                                                                           |
| `at`             | The time Bankroll recorded, ISO 8601.                                                                                           |

A timer is not cancelled: when it fires, look at what it names and decide
whether there is still anything to do.

`createTimer` throws `TimerError` with a `code`:

| Code                      | Meaning                                                        |
| ------------------------- | -------------------------------------------------------------- |
| `unauthenticated`         | No usable app key, or Bankroll refused the credential.         |
| `app_not_verified`        | The origin serves no Bankroll-signed manifest.                 |
| `webhook_not_provisioned` | The app was verified before webhooks existed; re-sign it.      |
| `invalid_argument`        | `meta` is not plain JSON, or `firesInMinutes` is out of range. |
| `unavailable`             | Bankroll could not be reached; nothing was created.            |

## Handle the event

`timer.fired` carries `id`, `meta` and `at`. The route hands it to `onFired`;
handle it idempotently, since a delivery can be retried.

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

async function onFired(event: TimerFired) {
  if (event.meta.kind !== "deadline") return;
  const round = await rounds.find(event.meta.roundId);
  if (round.status === "waiting") await rounds.forfeit(round.id);
}

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

## Under the mock

With `BANKROLL_MOCK=1` outside production nothing reaches Bankroll: the timer
is set locally and delivers `timer.fired` to your route itself, unsigned, when
it fires.
