> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.lipafy.xyz/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.lipafy.xyz/_mcp/server.

# Webhooks & Real-Time Event Telemetry

> Listen to payment lifecycle events, verify HMAC-SHA256 signatures, and handle retries.

Lipafy sends HTTPS POST webhooks in real time whenever payment intents transition lifecycle states or telecom receipts are reconciled.

---

## 🔔 Event Types

| Event Name                         | Trigger Condition                                                   |
| :--------------------------------- | :------------------------------------------------------------------ |
| `payment_intent.created`           | Intent drafted by an operator or agent.                             |
| `payment_intent.approval_required` | Intent exceeds policy threshold and requires human sign-off.        |
| `payment_intent.approved`          | Signatory signed the intent snapshot; queued for network execution. |
| `payment_intent.succeeded`         | Telecom network confirmed final settlement; receipt number issued.  |
| `payment_intent.failed`            | Transaction declined by network or rejected by policy engine.       |
| `stk_collection.succeeded`         | Customer entered M-Pesa PIN on their phone for chat checkout.       |

---

## 🔒 Verifying HMAC-SHA256 Signatures

Every webhook includes a `X-Lipafy-Signature` header computed using your workspace Webhook Secret:

```typescript
import crypto from 'crypto'

export function verifyLipafyWebhook(
  payloadRawBody: string,
  signatureHeader: string,
  webhookSecret: string
): boolean {
  const [timestampStr, signature] = signatureHeader.split(',').map((s) => s.split('=')[1])
  
  // Guard against replay attacks (reject payloads older than 5 minutes)
  const currentTime = Math.floor(Date.now() / 1000)
  if (Math.abs(currentTime - parseInt(timestampStr, 10)) > 300) {
    return false
  }

  const signedPayload = `${timestampStr}.${payloadRawBody}`
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(signedPayload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  )
}
```

---

## 📦 Webhook Payload Example (`payment_intent.succeeded`)

```json
{
  "id": "evt_0194820a-8401-7b92",
  "event": "payment_intent.succeeded",
  "created_at": "2026-08-17T00:30:00Z",
  "data": {
    "payment_intent": {
      "id": "pi_9f83a2bc-d14e-4b92-8051-18239014819a",
      "status": "SUCCEEDED",
      "amount_minor": "300000",
      "fee_minor": "0",
      "currency": "KES",
      "network_receipt": "QHK892N1P",
      "destination": "PayBill 150501 (Ref: 0722123456)",
      "purpose": "Monthly Home Fibre Renewal",
      "metadata": {
        "agent_id": "claude-code-home-pilot"
      }
    }
  }
}
```

Delivery ordering across distributed consumers is resilient against out-of-order deliveries. Applications should deduplicate events by event ID and inspect the payment intent state. Only `SUCCEEDED`, `FAILED`, or `CANCELLED` represent terminal finality; `PROCESSING` indicates an in-flight transaction.