# Yoco

Yoco takes card payments. callCash opens a Yoco checkout, sends your customer to Yoco's page, and believes the payment only once a signed Yoco event confirms it.

## What you need

- A Yoco account with online payments switched on.
- Your **secret key** from the Yoco portal. `sk_test_` keys run in test mode; any other key runs live.
- A **webhook** registered with Yoco that points at your Yoco webhook route, and the `whsec_` **secret** Yoco gives you for it. Yoco's own documentation shows how to register one.
- A **return URL** for customers to land on afterwards.

## Configure

```ts
export const cash = new CallCash({
  gateways: {
    yoco: { secretKey: process.env.YOCO_SECRET_KEY!, webhookSecret: process.env.YOCO_WEBHOOK_SECRET },
  },
  store,
  returnUrl: "https://example.co.za/paid",
});
```

Without `returnUrl` (or `return_url` on the call), creating a Yoco payment throws `config_invalid`. Without `webhookSecret`, Yoco events are refused with `config_invalid`, because they cannot be trusted.

## Taking a payment

```ts
const payment = await cash.payments.create({ amount: 250, reference: "order_1042", gateway: "yoco" });
// redirect the customer to payment.checkout_url
```

What callCash sends Yoco:

- The amount in cents, in ZAR.
- An `Idempotency-Key` equal to the payment id, so a retry after a dropped connection cannot open a second checkout.
- `metadata.callcash` set to the payment id, plus `metadata.reference`. Your own `metadata` goes along too, but cannot overwrite those two.
- Three return addresses built from your `returnUrl`, with `?status=success|cancelled|failed&reference=…&payment=…` added.

If Yoco answers 5xx or 429, callCash tries up to three times, then throws `gateway_down` (retryable) so routing can try your next gateway.

## The return URL is not proof

A customer arriving at `?status=success` tells you nothing you can trust: anyone can type that address. Show a "we're confirming your payment" page there, and fulfil the order only in your webhook handler.

## The webhook route

```ts
// app/api/yoco/webhook/route.ts
import { cash } from "@/lib/cash";
import { onCash } from "@/lib/on-cash";

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

What callCash checks before your handler runs:

- The `webhook-signature` header matches an HMAC-SHA256 of the event, using your `whsec_` secret.
- The event is no more than three minutes old (Yoco's recommendation), so a captured event cannot be replayed later.
- The event's mode matches your key, so a test event never reaches a live deployment, or the other way round.
- For a payment, the status is `succeeded`, the currency is ZAR and the amount matches. A different amount gives `payment.mismatch`, and the payment is not marked paid.

An event without `metadata.callcash` is not a callCash payment: `handle` returns `handled: false`, so older code can still process it.

Yoco events callCash understands: `payment.succeeded`, `payment.failed`, `refund.succeeded`, `refund.failed`.

## Refunds

Yoco refunds only work on payments made with **live** keys. With test keys, `refund()` throws `refund_unsupported`. See [Refunds](https://callcash.riverrr.co.za/docs/refunds).

## Test it

Use `sk_test_` keys and Yoco's test cards. Payments made this way have `mode: "test"`.
