Izzi FinanceDocumentation

Webhooks

Webhooks allow you to receive real-time HTTP notifications when events occur in your IZZIPAY account.

Webhook Setup

bash
curl -X POST https://api.izzipay.com/v1/developer/webhooks \
  -H "Authorization: Bearer sk_test_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/izzipay",
    "events": ["payment.succeeded", "payment.failed"],
    "secret": "whsec_your_signing_secret"
  }'

Events

EventDescription
payment.succeededA checkout payment or charge was captured.
payment.failedThe payment could not be completed.
payment.refundedA charge refund was recorded.
refund.rejectedThe institution rejected a refund.
checkout.session.completedThe checkout session reached SUCCEEDED.
checkout.session.expiredThe checkout session expired without a payment.
wallet.creditedThe merchant wallet balance increased.
payout.createdA payout was created.
payout.awaiting_approvalThe payout is waiting for institution approval.
payout.completedFunds were sent to the beneficiary.
payout.failedPayout execution failed.
payout.rejectedA reviewer rejected the payout.
settlement.createdA settlement batch was created.
settlement.completedThe settlement batch was closed.

Payload Format

json
{
  "id": "evt_abc123",
  "type": "payment.succeeded",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": "pay_xyz789",
    "amount": 5000,
    "currency": "USD",
    "status": "SUCCEEDED",
    "method": "mobile_money",
    "metadata": {}
  }
}

Signature Verification

Each delivery includes X-Izzi-Signature (HMAC-SHA256, hex) and X-Izzi-Timestamp (Unix seconds). Sign timestamp, a literal dot, then the raw body. Reject timestamps older than 300 seconds, and verify the signature before you parse the JSON.

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

function verifyIzziWebhook({ secret, rawBody, signature, timestampHeader }) {
  if (!signature || !timestampHeader) return false;

  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  if (expected.length !== signature.length) return false;
  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex"),
  );
}

app.post(
  "/webhooks/izzipay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const ok = verifyIzziWebhook({
      secret: process.env.IZZI_WEBHOOK_SECRET,
      rawBody,
      signature: req.headers["x-izzi-signature"],
      timestampHeader: req.headers["x-izzi-timestamp"],
    });

    if (!ok) return res.status(401).send("Invalid signature");

    const event = JSON.parse(rawBody);
    console.log(`Received: ${event.type}`);
    res.status(200).send("OK");
  },
);

Best Practices

  • •Always return a 200 response quickly to avoid timeouts
  • •Process webhook payloads asynchronously
  • •Implement idempotency: you may receive the same event multiple times
  • •Use the event ID to deduplicate
  • •Verify signatures before processing any event