# Webhooks and events

Each gateway calls a route in your app when something happens. You pass that request to callCash, and your handler gets one event shape, already checked with the gateway.

## One route per gateway

```ts
// app/api/yoco/webhook/route.ts
export async function POST(req: Request) {
  const r = await cash.webhooks.handle("yoco", req, onCash);
  return r.response;
}

// app/api/netcash/notify/route.ts
export async function POST(req: Request) {
  const r = await cash.webhooks.handle("netcash", req, onCash);
  return r.response;
}
```

**Always return `r.response` as it is.** Its status tells the gateway whether to send the notification again.

## One handler

```ts
// lib/on-cash.ts
import type { CashEvent } from "callcash";

export async function onCash(event: CashEvent) {
  switch (event.type) {
    case "payment.paid":
      await markOrderPaid(event.reference, event.amount); // must be safe to run twice
      break;
    case "payment.failed":
      await markOrderFailed(event.reference, event.reason);
      break;
    case "payment.mismatch":
      await alertSomeone(event.reference, event.reason);  // do not fulfil
      break;
  }
}
```

## What is checked before your handler runs

| Gateway | Check |
|---|---|
| Yoco | Signature, an age under three minutes, test or live mode matching your key |
| Netcash | The notification is unsigned, so Netcash's own status check is asked and only its answer is believed |
| Both | The confirmed amount must equal the payment's amount, or you get `payment.mismatch` and the payment stays unpaid |

Every event your handler receives has `verified: true`.

## Events

| Type | When |
|---|---|
| `payment.paid` | The gateway confirmed the full amount |
| `payment.failed` | The gateway says it failed or was declined. Ignored if the payment is already paid. |
| `payment.mismatch` | The gateway confirmed a different amount, or the payment was paid a second time. `reason` says which. |
| `refund.succeeded` | A refund went through, including one made in the gateway's portal |
| `refund.failed` | The gateway could not complete a refund |

## The event object

| Field | Meaning |
|---|---|
| `id` | The same on every redelivery. Use it to ignore repeats. |
| `type` | One of the types above |
| `payment` | The payment id |
| `reference` | Your order number |
| `amount` | Rand: the payment's amount, or the refund's on refund events |
| `method` | How the customer paid, when known |
| `gateway`, `mode` | Where it happened, test or live |
| `verified` | Always `true` |
| `refund` | The refund id, on refund events |
| `reason` | On failed and mismatch events |
| `redelivery` | `true` when an earlier attempt to give you this event failed |
| `created_at` | ISO timestamp |
| `data` | The whole payment, as it is now |

## Exactly once

- The same notification sent twice produces one event.
- Two notifications arriving at the same moment still run your handler once: an event is claimed while your handler runs. A claim lapses after a minute, so a crashed process does not strand the event.
- Even so, **write your handler to be safe to run twice** for the same `event.id`. Networks and processes fail in ways no library can rule out.

## When your handler throws

1. The payment is still recorded.
2. The gateway gets a **500**, so it sends the notification again.
3. The event stays owed. It comes back with the same `id` and `redelivery: true`.

Do not catch and hide errors inside your handler. Throwing is how the retry happens.

## A safety net: deliver what is owed

```ts
const { delivered, failed } = await cash.events.deliver(onCash);
```

This hands every event still owed to your handler. Run it from a cron job, or after your handler has been down. One payment failing does not stop the rest.

## The result of handle()

| Field | Meaning |
|---|---|
| `response` | Send it back to the gateway as it is |
| `handled` | `false` when the notification is not about a callCash payment |
| `ignored` | Why nothing happened, when nothing did |
| `events` | The events your handler received |
| `error` | What went wrong when the response is not a 200. Log it. |

Responses `handle` can give:

| Status | When |
|---|---|
| 200 | Done, or ignored (not ours, a repeat, a test event on a live key) |
| 400 | `signature_invalid`: the Yoco event's signature or age failed |
| 500 | `config_invalid` (a missing secret or gateway), `handler_failed` (your handler threw), or the request could not be read |
| 503 | `gateway_down` (Netcash's status check could not be reached) or `api_error` (the store failed): the gateway should retry |

## Running beside older payment code

When `handled` is `false`, the notification belongs to something callCash did not create, such as checkouts your old code opened. Pass it on:

```ts
export async function POST(req: Request) {
  const r = await cash.webhooks.handle("yoco", req.clone(), onCash);
  if (r.handled) return r.response;
  return oldYocoHandler(req); // unchanged
}
```

Use `req.clone()` so the old code can still read the body.

## Listing events

```ts
const events = await cash.events.list({ after: "evt_…", limit: 100 });
```

Oldest first. `after` continues from an event id.

## Who retries, and for how long

Today callCash runs inside your server, so callCash doesn't send you webhooks. Yoco and Netcash send them to your routes, and `cash.webhooks.handle()` checks each one before your handler sees it.

- **If your handler throws,** we answer the gateway with a 500. The gateway sends the notification again on its own retry schedule, which belongs to Yoco or Netcash, not to us. The event keeps its `id` and arrives with `redelivery: true`.
- **If the gateway gives up,** the event is still owed in your store. Run `cash.events.deliver(onCash)` from a cron, every few minutes, and nothing is lost.
- **The hosted service is planned.** It will send its own webhooks, and its retry window and schedule will be published here before it handles a payment.

## When the money arrives

callCash never holds money, so settlement is between your gateway and your bank, on the gateway's payout terms. Check your Yoco or Netcash agreement for when payouts happen. A `payment.paid` event means the gateway confirmed the payment. It doesn't mean the money has reached your bank yet.
