Skip to main content

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

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:
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:
{
  "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": { ... }
}
FieldTypeDescription
api_versionstringDate-versioned schema (e.g., "2026-03-27")
event_idstringGlobally unique ID (evt_{uuid4}) — use for idempotency
event_typestringDot-notation event name (e.g., payment.succeeded)
occurred_atstringISO 8601 UTC timestamp
is_livebooleanfalse for sandbox/test events
business_idstringMerchant’s platform ID
dataobjectEvent-specific payload (see Events)
The event_id is the idempotency key. You must store processed event_id values and skip duplicates — retries reuse the same event_id.

Webhook Headers

Every webhook request includes these headers:
HeaderDescription
X-Khaime-EventEvent type (e.g., payment.succeeded)
X-Khaime-Event-IdUnique event ID (matches event_id in the body)
X-Khaime-SignatureHMAC-SHA256 signature for verification
X-Khaime-Api-VersionAPI version for this delivery (e.g., 2026-03-27)
Content-Typeapplication/json

Delivery

PropertyValue
Timeout10 seconds
Retries3 attempts
Retry delay2 seconds between attempts
SuccessAny HTTP 2xx response
Respond to webhooks within 5 seconds with a 200 status. Process the event asynchronously after acknowledging receipt.

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.
{
  "amount": 5000,
  "currency": "USD"
}

MulticurrencyBreakdown

Present on payment and order events. Provides a full breakdown of amounts, fees, and currency conversion.
{
  "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

{
  "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

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);
});