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

# Webhook deliveries

> How Kwugwo sends events to your endpoint: payload shape, signing, retries.

You register webhook endpoints in the dashboard. From then on, every
qualifying event in your workspace is delivered to that endpoint as
an HTTP `POST`. This page explains the request shape, how to verify
it, and how retries work.

<Note>
  Endpoint management (creating, listing, regenerating secrets) is
  done from the dashboard. The `/v1/*` merchant API does not expose
  webhook CRUD.
</Note>

## Event types

| Event                   | Fires when                                                                       |
| ----------------------- | -------------------------------------------------------------------------------- |
| `nzube.onye.created`    | A customer record is created.                                                    |
| `nzube.onye.updated`    | A customer record is updated.                                                    |
| `nzube.onye.deleted`    | A customer record is deleted.                                                    |
| `ugwo.created`          | A new ugwo is created.                                                           |
| `ugwo.updated`          | An ugwo changes state (e.g. `requires_ugwo` → `processing` → `ugwo_successful`). |
| `ugwo.activity.created` | An activity is created against an ugwo.                                          |
| `ugwo.activity.updated` | An activity changes state (`requires_action` → `successful`, etc).               |

## Delivery request

Every webhook is sent as an HTTP `POST` with a JSON body.

### Headers

| Header                 | Purpose                                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`         | Always `application/json`.                                                                                              |
| `X-Kwugwo-Signature`   | HMAC-SHA256 of the raw body, hex-encoded, computed with your endpoint's secret. Empty string if you never set a secret. |
| `X-Kwugwo-Delivery-Id` | UUID identifying this **delivery attempt**. Re-delivery on retry uses a new id. Useful for idempotency on your side.    |

### Body envelope

The body is an envelope wrapping the resource that triggered the event:

```json theme={null}
{
  "uid": "evt.VCvr.7K2qPmRtV9xLnQ8sD1cYwHfE",
  "event": "ugwo.activity.updated",
  "data": {
    /* The resource snapshot. For ugwo.* events this is an ugwo;
       for ugwo.activity.* events this is an activity; for
       nzube.onye.* events this is an onye. */
  }
}
```

* **`uid`**: the **event** id. It follows Kwugwo's standard
  [ID format](/ids-and-prefixes) with the `evt` prefix and is
  **stable across redelivery attempts**; use it as your
  idempotency key.
* **`event`**: the event type from the table above.
* **`data`**: the resource snapshot, in the same shape the
  corresponding `GET` endpoint returns.

## Verifying the signature

Compute `HMAC-SHA256(secret, raw_body)` and compare it constant-time
against the `X-Kwugwo-Signature` header. **Verify against the raw
body bytes**, not a re-encoded JSON string; re-serializing will
change whitespace and break the signature.

<CodeGroup>
  ```js Node theme={null}
  import crypto from "node:crypto";

  export function verifyKwugwoSignature(rawBody, header, secret) {
    if (!secret) return true; // signature is empty when you've not set a secret
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(header, "hex"),
    );
  }
  ```

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

  def verify_kwugwo_signature(raw_body: bytes, header: str, secret: str) -> bool:
      if not secret:
          return True
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, header)
  ```

  ```php PHP theme={null}
  function verify_kwugwo_signature(string $rawBody, string $header, string $secret): bool {
      if ($secret === '') return true;
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $header);
  }
  ```
</CodeGroup>

<Warning>
  If your framework reads the body before you do (e.g. Express's
  `body-parser`), make sure you can still get at the **raw** bytes.
  In Express, attach `bodyParser.json({ verify: (req, _res, buf) => req.rawBody = buf })`
  and verify against `req.rawBody`.
</Warning>

## Source IP

All Kwugwo outbound traffic - both webhook deliveries and any
server-to-server API calls we make on your behalf - originates from
a single static IP:

```
176.97.192.227
```

If your endpoint sits behind an allowlist, whitelist this address so
Kwugwo can reach it. The same IP is used in sandbox and live.

## Retries

Kwugwo treats any `2xx` response from your endpoint as success. Any
other status, a redirect, a network error, or a timeout is a failure.

| Setting          | Value                                                                           |
| ---------------- | ------------------------------------------------------------------------------- |
| Max attempts     | **3** (the initial delivery + 2 retries)                                        |
| Retry interval   | **30 minutes** between attempts                                                 |
| Backoff          | Fixed (no exponential growth)                                                   |
| Per-event status | `pending` → `success` on a 2xx, or `pending` again until the cap, then `failed` |

After the third failed attempt the event is marked `failed` and is
not retried again. Re-delivery is not yet exposed in the dashboard;
if you need an event replayed, contact support with the event `uid`.

## Best practices

* **Respond quickly.** Return `200` as soon as you've persisted the
  event UID. Do the heavy work asynchronously; webhook delivery
  treats any timeout as a failure and burns one of your three
  attempts.
* **Be idempotent on `event.uid`.** Two attempts of the same event
  share the same `uid`. Two events for the same state change
  (e.g. `ugwo.updated` followed by `ugwo.activity.updated`) do not.
* **Re-fetch on the merchant API for the source of truth.** The
  `data` snapshot is what the resource looked like at the moment the
  event fired. If your handler runs minutes later (or after a
  retry), pull the latest from the merchant API before acting.
* **Don't enforce a list of allowed event types in code.** New
  events get added over time; just skip the ones you don't care
  about.
