Webhooks
Webhooks allow your application to receive real-time notifications when events occur in your Cashfin account.
Overview
Instead of polling the API for updates, webhooks push data to your application when:
- A payment is completed
- An order status changes
- A subscription is created or updated
- And more...
Setting Up Webhooks
Step 1: Create a Webhook Endpoint
Create an endpoint on your server to receive webhook events:
javascript
// Express.js example
app.post("/webhooks/cashfin", express.json(), (req, res) => {
const event = req.body;
// Process the webhook event
switch (event.type) {
case "payment.completed":
handlePaymentCompleted(event.data);
break;
case "order.created":
handleOrderCreated(event.data);
break;
// ... handle other events
}
// Acknowledge receipt
res.status(200).json({ received: true });
});Step 2: Register Your Webhook
- Go to Settings → Webhooks in your dashboard
- Click Add Endpoint
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive
- Save the configuration
Step 3: Verify Webhook Signatures
Verify that webhooks are genuinely from Cashfin:
javascript
const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post(
"/webhooks/cashfin",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-cashfin-signature"];
const webhookSecret = process.env.CASHFIN_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
// Process event...
res.status(200).json({ received: true });
}
);Webhook Events
Payment Events
| Event | Description |
|---|---|
payment.initiated | Payment process started |
payment.completed | Payment successfully completed |
payment.failed | Payment attempt failed |
payment.refunded | Payment was refunded |
Order Events
| Event | Description |
|---|---|
order.created | New order created |
order.processing | Order is being processed |
order.completed | Order fulfilled |
order.cancelled | Order was cancelled |
Subscription Events
| Event | Description |
|---|---|
subscription.created | New subscription created |
subscription.activated | Subscription became active |
subscription.renewed | Subscription renewed |
subscription.cancelled | Subscription cancelled |
subscription.expired | Subscription expired |
Customer Events
| Event | Description |
|---|---|
customer.created | New customer created |
customer.updated | Customer information updated |
Event Payload Structure
All webhook events follow this structure:
json
{
"id": "evt_507f1f77bcf86cd799439011",
"type": "payment.completed",
"created": "2024-01-15T10:30:00.000Z",
"data": {
"id": "pay_507f191e810c19729de860ea",
"amount": 5000.0,
"currency": "KES",
"status": "completed"
// ... event-specific data
},
"business_id": "507f1f77bcf86cd799439011"
}Event Fields
| Field | Description |
|---|---|
id | Unique event identifier |
type | Event type (e.g., payment.completed) |
created | ISO 8601 timestamp of when the event occurred |
data | Event-specific payload |
business_id | Your business ID |
Example Payloads
Payment Completed
json
{
"id": "evt_abc123",
"type": "payment.completed",
"created": "2024-01-15T10:30:00.000Z",
"data": {
"id": "pay_xyz789",
"transaction_id": "txn_123456",
"amount": 5000.0,
"currency": "KES",
"payment_method": "mpesa",
"customer": {
"id": "cust_456",
"name": "John Doe",
"phone": "+254712345678"
},
"reference": "ORD-001",
"metadata": {}
}
}Order Created
json
{
"id": "evt_def456",
"type": "order.created",
"created": "2024-01-15T11:00:00.000Z",
"data": {
"id": "ord_789",
"order_no": "ORD-2024-001",
"status": "processing",
"items": [
{
"product_id": "prod_123",
"title": "Widget Pro",
"quantity": 2,
"price": 1500.0
}
],
"total": 3000.0,
"customer_email": "[email protected]"
}
}Subscription Created
json
{
"id": "evt_ghi789",
"type": "subscription.created",
"created": "2024-01-15T12:00:00.000Z",
"data": {
"id": "sub_abc",
"subscription_no": "SUB-2024-001",
"status": "trial",
"billing_cycle": "monthly",
"amount": 999.0,
"trial": {
"start_date": "2024-01-15T00:00:00.000Z",
"end_date": "2024-01-29T23:59:59.000Z"
},
"next_billing_date": "2024-01-30T00:00:00.000Z",
"customer": {
"id": "cust_123",
"name": "Jane Doe",
"email": "[email protected]"
}
}
}Best Practices
1. Respond Quickly
Return a 200 status as soon as possible. Process webhooks asynchronously:
javascript
app.post("/webhooks/cashfin", (req, res) => {
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
setImmediate(() => {
processWebhook(req.body);
});
});2. Handle Duplicates
Webhooks may be retried. Use the event id to detect duplicates:
javascript
const processedEvents = new Set();
function processWebhook(event) {
if (processedEvents.has(event.id)) {
return; // Already processed
}
processedEvents.add(event.id);
// Process the event...
}3. Verify Signatures
Always verify webhook signatures to ensure authenticity:
javascript
if (!verifySignature(req)) {
return res.status(401).json({ error: "Invalid signature" });
}4. Implement Retry Logic
If your endpoint is unavailable, Cashfin will retry:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 24 hours |
5. Log All Events
Keep a log of all webhook events for debugging:
javascript
app.post("/webhooks/cashfin", (req, res) => {
console.log("Webhook received:", {
id: req.body.id,
type: req.body.type,
timestamp: new Date().toISOString(),
});
// Process...
});Testing Webhooks
Using the Dashboard
- Go to Settings → Webhooks
- Click Send Test Event
- Select an event type
- Click Send
Local Development
Use tools like ngrok to expose your local server:
bash
# Start ngrok
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/cashfinTroubleshooting
Webhook Not Received
- Check your endpoint is publicly accessible
- Verify the URL uses HTTPS
- Check server logs for errors
- Ensure your server responds within 30 seconds
Invalid Signature
- Use the correct webhook secret
- Verify you're using the raw request body
- Check for encoding issues
Missing Events
- Verify the event type is enabled in dashboard
- Check webhook logs in the dashboard
- Ensure your endpoint returns 200 status