# Yoco Webhooks

Yoco (South African card machines, POS, and online payments) sends webhooks from
two APIs: the **Yoco API** (payments, orders, catalogue) and the **Checkout API**
(hosted checkout payments and refunds). Both sign deliveries with
Standard Webhooks: HMAC-SHA256 over
`{webhook-id}.{webhook-timestamp}.{rawBody}` with a `whsec_` secret.

## When to Use This Skill

* How do I receive Yoco webhooks in Express, Next.js, or FastAPI?
* How do I verify Yoco webhook signatures?
* How do I handle `payment.created`, `payment.refunded`, or `order.completed` events?
* How do I confirm a Checkout API payment with `payment.succeeded`?
* Why is my Yoco webhook signature verification failing?
* Where do I get the Yoco webhook secret?

## Signature at a Glance

| Property | Value |
| --- | --- |
| Headers | `webhook-id`, `webhook-timestamp`, `webhook-signature` |
| Algorithm | HMAC-SHA256, base64-encoded |
| Signed content | `{webhook-id}.{webhook-timestamp}.{rawBody}` |
| Key | Strip `whsec_` from the secret, base64-decode the rest |
| Signature header | Space-separated list of `v1,<base64sig>`; accept if any matches |
| Timestamp tolerance | Yoco recommends 3 minutes |
| Secret | Returned **once**, in the create-subscription response |

## Verification (core)

Yoco recommends the Standard Webhooks library (`standardwebhooks` on npm and
PyPI). It defaults to a 5-minute window, so check Yoco's 3 minutes first.
**Always pass the raw request body.**

```javascript
const { Webhook } = require('standardwebhooks');

function verifyYocoWebhook(rawBody, headers, secret) {
  const ts = Number(headers['webhook-timestamp']);
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 180) {
    throw new Error('Timestamp outside the 3-minute tolerance');
  }
  // Strips "whsec_", base64-decodes the key, HMACs "{id}.{ts}.{body}",
  // and compares each "v1," signature in constant time. Throws on mismatch.
  return new Webhook(secret).verify(rawBody, headers);
}

```

```python
from standardwebhooks.webhooks import Webhook

if abs(int(time.time()) - int(headers["webhook-timestamp"])) > 180:
    raise ValueError("Timestamp outside the 3-minute tolerance")
event = Webhook(secret).verify(raw_body, headers)  # raises on mismatch -> return 400

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/yoco-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/yoco-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/yoco-webhooks/examples/fastapi/).

A manual (library-free) implementation is in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/yoco-webhooks/references/verification.md).

## Common Event Types

Yoco API events carry `event_type`; Checkout API events carry `type`. Read
`event.event_type ?? event.type` if one endpoint receives both.

| Event | API | Triggered When |
| --- | --- | --- |
| `payment.created` | Yoco API | A payment is created |
| `payment.refunded` | Yoco API | A payment is refunded |
| `order.created` / `order.updated` | Yoco API | An order is opened / changed |
| `order.completed` / `order.cancelled` | Yoco API | An order is fully paid / cancelled |
| `item.*`, `item_category.*` | Yoco API | Catalogue items or categories are created, updated, or deleted |
| `payment.succeeded` / `payment.failed` | Checkout API | A checkout payment succeeds / fails |
| `refund.succeeded` / `refund.failed` | Checkout API | A refund succeeds / fails |

Checkout API webhooks fire for **every** payment on the account. Match
`payload.metadata.checkoutId` to your checkout before fulfilling. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/yoco-webhooks/references/overview.md) for payloads.

## Environment Variables

```bash
YOCO_WEBHOOK_SECRET=whsec_xxxxx   # From the create-subscription response (shown once)

```

## Local Development

Receive webhooks locally with the Hookdeck CLI — no account required, one paste-and-run line:

```bash
npx hookdeck-cli listen 3000 yoco --path /webhooks/yoco

```

Register the printed URL as your subscription's `notification_url` (Yoco API) or
`url` (Checkout API). With the Yoco API, `POST /v1/webhooks/subscriptions/{id}/test`
sends a signed sample event.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/yoco-webhooks/references/overview.md) - Yoco API vs Checkout API webhooks, full event list, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/yoco-webhooks/references/setup.md) - Create a subscription, store the secret, rotate it, send test events
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/yoco-webhooks/references/verification.md) - Signature details, SDK and manual verification, debugging