Skip to content

Webhooks & Signature Verification

Webhooks deliver real-time asynchronous notifications when payment statuses change (e.g. mobile money STK push confirmed, CBE Birr payment received).


πŸ›‘οΈ HMAC Signature & Verification Lifecycle

Section titled β€œπŸ›‘οΈ HMAC Signature & Verification Lifecycle”
sequenceDiagram
autonumber
participant Provider as πŸ“± Telco / Bank Gateway
participant Zirzir as ⚑ Zirzir Rust Server
participant Backend as πŸ–₯️ Your Webhook Handler
Provider->>Zirzir: Provider Webhook Callback
Zirzir->>Zirzir: Validates provider payload & updates transaction
Zirzir->>Zirzir: Computes HMAC-SHA256: t=timestamp, v1=HMAC(secret, "t.body")
Zirzir->>Backend: POST /webhooks/zirzir with header X-Zirzir-Signature
alt Valid Signature & Timestamp within 300s
Backend-->>Zirzir: 200 OK
Backend->>Backend: Asynchronously fulfill customer order
else Replay Attack or Invalid Secret
Backend-->>Zirzir: 401 Unauthorized
end

Every outbound webhook includes the X-Zirzir-Signature header:

X-Zirzir-Signature: t=1788319200,v1=52f08a478b273b5c3e5...
  • t: Unix timestamp (in seconds). Protects against replay attacks.
  • v1: Hex-encoded HMAC-SHA256 signature calculated over ${t}.${raw_body} using your Webhook Secret.

import { Zirzir } from '@zirzir/sdk';
import express from 'express';
const app = express();
const zirzir = new Zirzir({ baseUrl: 'http://localhost:8080', apiKey: 'zz_test_...' });
app.post('/webhooks/zirzir', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-zirzir-signature'] as string;
const secret = process.env.ZIRZIR_WEBHOOK_SECRET!;
try {
const event = zirzir.webhooks.constructEvent(req.body, signature, secret);
console.log('βœ… Verified Event:', event.type, event.data.reference);
res.status(200).json({ received: true });
} catch (err) {
console.error('❌ Signature Verification Failed:', err);
res.status(400).send(`Webhook Error: ${(err as Error).message}`);
}
});