> ## Documentation Index
> Fetch the complete documentation index at: https://partners.ladder.africa/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Signed HTTPS deliveries for every money and order event, retried for 24 hours, verifiable in five lines.

Register up to five HTTPS endpoints per environment. The signing secret is shown once.

```json Request theme={null}
{
  "url": "https://hooks.yourcompany.com/ladder",
  "description": "Production receiver",
  "events": ["wallet.funded", "order.created", "order.completed", "order.cancelled"]
}
```

```json Response theme={null}
{
  "data": {
    "id": "b5898de8-7988-4f2a-9c51-c51b3a1e9885",
    "url": "https://hooks.yourcompany.com/ladder",
    "events": ["wallet.funded", "order.created", "order.completed", "order.cancelled"],
    "enabled": true,
    "secret": "whsec_…"
  }
}
```

Leave `events` empty to receive everything. `POST /webhooks/{id}/test` sends a `webhook.test` synchronously so you can check your receiver before anything real happens.

## What a delivery looks like

```
POST https://hooks.yourcompany.com/ladder
X-Ladder-Webhook-Id:        evt_84b183e0f1834a899e5d221969505234
X-Ladder-Webhook-Event:     order.completed
X-Ladder-Webhook-Signature: t=1760214578,v1=<hex HMAC-SHA256(secret, t + "." + raw_body)>

{"id": "evt_…", "type": "order.completed", "created_at": "…", "environment": "live", "data": { …the order… }}
```

`data` is the same object the API returns for that resource, so one parser serves both.

## Verify before you parse

```python theme={null}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, received = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)
```

Verify against the **raw** request body, before parsing it.

## Operational rules

* Respond `2xx` within a few seconds and process asynchronously.
* **Deduplicate on `id`.** Retries resend the same event, and a network blip can deliver one twice.
* Anything other than `2xx` is retried at 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (eight attempts). `GET /webhook-deliveries` shows every attempt with the last status code and error; `POST /webhook-deliveries/{id}/retry` resends on demand.
* Rotate a secret with `POST /webhooks/{id}/rotate-secret`; the live URL must be `https` and publicly reachable.

## Events

| event | when |
| - | - |
| `wallet.funded` | a funding request settled; `data` is the funding request with `amount_received` |
| `funding.failed`, `funding.expired` | the provider reported failure, or `expires_at` passed |
| `mandate.activated`, `mandate.revoked` | a direct-debit mandate changed state |
| `order.created`, `order.processing`, `order.completed`, `order.failed`, `order.cancelled` | the order lifecycle |
| `holding.matured` | a holding reached maturity |
| `kyb.status_changed` | your KYB moved |
| `webhook.test` | sent by `POST /webhooks/{id}/test` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.