# Handle the result

Two ways to learn a payment settled — poll GET /status, or receive a signed webhook. How to choose, and how to verify.

`collect()` returns before the customer has typed their PIN. There are two ways to learn what happened, and both read the same settled record.

| | Polling | Webhooks |
| --- | --- | --- |
| You host | Nothing | An HTTPS endpoint |
| Good for | Scripts, a checkout with a spinner | Servers, volume, back-office fulfilment |

Start with polling. Move to webhooks when you have a server that wants to be told.

## Option A: poll

```ts title="poll.ts"
const outcome = await paylod.wait(ack.paymentId, { timeoutMs: 120_000 });

if (outcome.paid) await fulfil(outcome.receipt);
else              await notify(outcome.message);
```

`wait()` polls on a jittered backoff ramp until the payment settles. If the payment is still pending, paylod runs a live M-Pesa STK Query and settles it on the spot, so a poll never returns a stale answer.

`paylod.status(paymentId)` reads the record once, without polling.

> A timeout **throws** `PaylodTimeoutError` rather than returning `ok: false`. An unanswered prompt is not a failed payment: the customer may still be typing their PIN, and it can still succeed. Do not mark the order failed. Let the webhook tell you.

## Option B: receive a signed webhook

Register an endpoint on the application's **Endpoints & Webhooks** tab. The SDK verifies the signature for you.

```ts title="app/api/webhooks/paylod/route.ts"
import { Paylod } from "@paylod/node";

const paylod = new Paylod();

export const POST = paylod.webhookHandler(async (event) => {
  if (event.type === "payment.success") {
    await fulfil(event.data.paymentId, event.data.mpesaReceipt);
  } else if (event.data.decoded) {
    await notify(event.data.decoded.customerMessage);
  }
});
```

`decoded` is `DecodedError | null`: it is populated on `payment.failed` and `null` on `payment.success`. Check it before you read it.

This works anywhere the Web `Request`/`Response` API does: Next.js, Hono, Remix, Workers, Bun, Deno. On Express, use `paylod.webhook(handler)` instead.

The signature is checked **before** your handler runs. A bad signature returns `400` and your handler never sees the event. If your handler throws, the route returns `500` and paylod retries the delivery, so make the handler idempotent.

> [!warn] On Express, mount this route **before** any global `express.json()`. Once a JSON parser has consumed the body the raw bytes are gone and no signature can be verified. The middleware detects this and fails loudly rather than skipping the check.

## Check the amount before you fulfil

The webhook carries the **true settled amount**. Fulfilling on `status` alone is how merchants get underpaid.

```ts title="fulfil.ts"
export const POST = paylod.webhookHandler(async (event) => {
  if (event.type !== "payment.success") return;

  // Look the order up by paymentId — the id you stored when you called collect().
  const order = await db.orders.findByPaymentId(event.data.paymentId);
  if (!order) return;

  if (event.data.amount === order.amountKes) {
    await fulfil(order);
  } else {
    await flagForReview(order, event.data);   // underpaid
  }
});
```

Store `ack.paymentId` against your order when you send the push, and key the lookup on it. The event does not echo back the `metadata` you passed to `collect()`.

## Failures are results, not outages

`payment.failed` is a normal outcome. `event.data.decoded` is already decoded for you:

- `1032` the customer pressed Cancel. Offer a retry.
- `1037` the prompt never reached the handset, or was ignored.
- `2001` wrong M-Pesa PIN.
- `1` insufficient balance.

`4999` is **not** a failure. It means the customer has not entered their PIN yet. Keep polling, and never re-collect: a second push fires a second prompt and can double-charge. Every code is in the [error reference](/docs/errors).

## Verify it yourself

If you are not using the SDK, the signature is an HMAC-SHA256 over `t + "." + rawBody`, keyed by the endpoint's signing secret, with a 5-minute freshness window. Compute it over the raw bytes, never a re-serialised object. The scheme is specified in the [webhooks reference](/docs/api/webhooks).

## Next

[Test in sandbox](/docs/guides/test-in-sandbox) forces each of these outcomes on demand.
