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

# Webhooks Overview

> Receive real-time notifications for payments, subscriptions, wallet operations, settlements, disputes, and order fulfillment.

# Webhooks

Khaime sends HTTP POST requests to your webhook URL when events occur across the full lifecycle of payments, subscriptions, wallet operations, settlements, disputes, and physical order fulfillment. This is the most reliable way to track transaction state — don't rely solely on redirect callbacks.

## Setup

1. Go to **Khaime Dashboard → Settings → API**
2. Set your **Webhook URL** (e.g., `https://yoursite.com/webhooks/khaime`)
3. Copy your **Webhook Secret** (`whsec_...`)

Or set it programmatically:

```bash theme={null}
curl -X PUT https://api.khaime.com/api/v1/partner/api-keys/YOUR_KEY_ID/webhook \
  -H "x-id-key: YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url": "https://yoursite.com/webhooks/khaime"}'
```

## Envelope Structure

Every webhook delivery shares the same outer envelope regardless of event type:

```json theme={null}
{
  "api_version": "2026-03-27",
  "event_id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "event_type": "payment.succeeded",
  "occurred_at": "2026-03-27T14:32:00Z",
  "is_live": true,
  "business_id": "1042",
  "data": { ... }
}
```

| Field         | Type    | Description                                              |
| ------------- | ------- | -------------------------------------------------------- |
| `api_version` | string  | Date-versioned schema (e.g., `"2026-03-27"`)             |
| `event_id`    | string  | Globally unique ID (`evt_{uuid4}`) — use for idempotency |
| `event_type`  | string  | Dot-notation event name (e.g., `payment.succeeded`)      |
| `occurred_at` | string  | ISO 8601 UTC timestamp                                   |
| `is_live`     | boolean | `false` for sandbox/test events                          |
| `business_id` | string  | Merchant's platform ID                                   |
| `data`        | object  | Event-specific payload (see [Events](/webhooks/events))  |

<Note>
  The `event_id` is the idempotency key. You **must** store processed `event_id` values and skip duplicates — retries reuse the same `event_id`.
</Note>

## Webhook Headers

Every webhook request includes these headers:

| Header                 | Description                                        |
| ---------------------- | -------------------------------------------------- |
| `X-Khaime-Event`       | Event type (e.g., `payment.succeeded`)             |
| `X-Khaime-Event-Id`    | Unique event ID (matches `event_id` in the body)   |
| `X-Khaime-Signature`   | HMAC-SHA256 signature for verification             |
| `X-Khaime-Api-Version` | API version for this delivery (e.g., `2026-03-27`) |
| `Content-Type`         | `application/json`                                 |

## Delivery

| Property    | Value                      |
| ----------- | -------------------------- |
| Timeout     | 10 seconds                 |
| Retries     | 3 attempts                 |
| Retry delay | 2 seconds between attempts |
| Success     | Any HTTP 2xx response      |

<Warning>
  Respond to webhooks within **5 seconds** with a `200` status. Process the event asynchronously after acknowledging receipt.
</Warning>

## Shared Data Types

These types appear across multiple event payloads.

### MoneyAmount

All monetary values are in the **smallest currency unit** (cents for USD, kobo for NGN). Never floats.

```json theme={null}
{
  "amount": 5000,
  "currency": "USD"
}
```

### MulticurrencyBreakdown

Present on payment and order events. Provides a full breakdown of amounts, fees, and currency conversion.

```json theme={null}
{
  "customer_paid": { "amount": 5000000, "currency": "NGN" },
  "merchant_gross": { "amount": 306, "currency": "USD" },
  "merchant_net": { "amount": 270, "currency": "USD" },
  "fees": {
    "platform_fee": { "amount": 18, "currency": "USD" },
    "gateway_fee": { "amount": 18, "currency": "USD" },
    "total": { "amount": 36, "currency": "USD" }
  },
  "conversion": {
    "from_currency": "NGN",
    "to_currency": "USD",
    "rate": 1630.5,
    "surcharge_percent": 1.0
  }
}
```

If the customer and merchant currencies are the same, `conversion` is omitted and `customer_paid` equals `merchant_gross`.

### Customer

```json theme={null}
{
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Doe",
  "id": "cust_123"
}
```

The `id` field is present only if the customer has a platform account.

## Example Handler

<CodeGroup>
  ```javascript Express.js theme={null}
  app.post('/webhooks/khaime', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-khaime-signature'];
    const rawBody = req.body.toString();
    const payload = JSON.parse(rawBody);

    // 1. Verify signature (use raw body, not re-serialized)
    const expected = crypto
      .createHmac('sha256', process.env.KHAIME_WEBHOOK_SECRET)
      .update(rawBody)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return res.status(401).send('Invalid signature');
    }

    // 2. Check for duplicate using event_id
    if (await isDuplicate(payload.event_id)) {
      return res.status(200).send('Already processed');
    }

    // 3. Acknowledge immediately
    res.status(200).send('OK');

    // 4. Process asynchronously
    processEvent(payload);
  });
  ```

  ```python Flask theme={null}
  @app.route('/webhooks/khaime', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Khaime-Signature')
      raw_body = request.get_data(as_text=True)
      payload = json.loads(raw_body)

      # Verify using raw body
      expected = hmac.new(
          WEBHOOK_SECRET.encode(),
          raw_body.encode(),
          hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(signature, expected):
          abort(401)

      # Process
      handle_event(payload)
      return '', 200
  ```

  ```php WordPress theme={null}
  add_action('rest_api_init', function() {
      register_rest_route('khaime/v1', '/webhook', [
          'methods' => 'POST',
          'callback' => 'handle_khaime_webhook',
      ]);
  });

  function handle_khaime_webhook($request) {
      $signature = $request->get_header('X-Khaime-Signature');
      $raw_body = $request->get_body();
      $payload = json_decode($raw_body, true);

      $expected = hash_hmac('sha256', $raw_body, WEBHOOK_SECRET);

      if (!hash_equals($expected, $signature)) {
          return new WP_Error('invalid_signature', '', ['status' => 401]);
      }

      // Process event...
      return new WP_REST_Response(['status' => 'ok'], 200);
  }
  ```
</CodeGroup>
