Webhooks

Webhooks allow your application to receive real-time updates about your BessaPay transactions. Instead of polling the API for updates, webhooks push events to your application as they happen.

When an event occurs, we'll send an HTTP POST request to the endpoint you've configured with a JSON payload describing the event.

Setting Up Webhooks

To set up webhooks, provide a webhook URL when registering as a developer or update your webhook URL in the BessaPay Dashboard.

Your endpoint should:

  • Be publicly accessible over the internet
  • Respond with a 2xx status code (preferably 200)
  • Process the webhook asynchronously to respond quickly
  • Verify the webhook signature to ensure it came from BessaPay

💡 Environment-Specific Webhooks:

You can set up different webhook URLs for TEST and LIVE environments. This helps you separate test events from production events. Configure these separately in the BessaPay Dashboard under each environment section.

Webhook Payloads

All webhook payloads share a consistent structure:

Webhook Payload Structure
1{
2  "event": "payment.success",
3  "transactionId": "txn_1J3F9KHGKm7Zf",
4  "merchantId": "mer_1J3F9KHGKm7Zf",
5  "developerTransactionReference": "order-123456",
6  "amount": 1000,
7  "currency": "KES",
8  "status": "COMPLETED",
9  "paymentStatus": "COMPLETED",
10  "timestamp": "1623423423",
11  "transactionReference": "TXN12345",
12  "gatewayTransactionId": "12345",
13  "gatewayResponseCode": "000",
14  "gatewayResponseMessage": "Success",
15  "environment": "TEST"
16}

Environment Indicator

All webhooks include an environment field that indicates which environment (TEST or LIVE) the event originated from. This allows your application to handle test and production events differently.

Webhook Headers

In addition to the payload, webhooks include environment information in the headers:

  • X-BessaPay-Environment: TEST or X-BessaPay-Environment: LIVE

Event Types

BessaPay sends the following event types:

payment.success

Sent when a payment has been successfully completed.

payment.failed

Sent when a payment has failed.

payment.pending

Sent when a payment is pending authorization or processing.

payment.timeout

Sent when a payment has timed out or expired.

payment.update

Sent when there's an update to the payment that doesn't fit other categories.

Webhook Signatures

To secure your webhooks, BessaPay signs all webhook requests with a unique secret key. You should verify this signature to ensure the webhook came from BessaPay.

The signature is included in the X-BessaPay-Signature header of the request. It's created using HMAC-SHA256 with your webhook secret.

⚠️ Environment-Specific Secrets:

TEST and LIVE environments have different webhook secrets. Always use the correct secret for the environment specified in the X-BessaPay-Environment header when verifying signatures.

Verifying Webhook Signatures (Node.js)
1const crypto = require('crypto');
2const express = require('express');
3const app = express();
4app.use(express.json());
5
6// Your webhook secrets from the BessaPay Dashboard
7const webhookSecrets = {
8  'TEST': 'whsec_test_1234567890abcdef',
9  'LIVE': 'whsec_live_1234567890abcdef'
10};
11
12// BessaPay webhook handler
13app.post('/webhooks/bessapay', (req, res) => {
14  const signature = req.headers['x-bessapay-signature'];
15  const environment = req.headers['x-bessapay-environment'] || 'TEST';
16  const payload = JSON.stringify(req.body);
17  
18  // Select the correct secret based on environment
19  const webhookSecret = webhookSecrets[environment];
20  
21  if (!webhookSecret) {
22    return res.status(400).send('Unknown environment');
23  }
24  
25  if (!verifySignature(payload, signature, webhookSecret)) {
26    return res.status(400).send('Invalid signature');
27  }
28  
29  // Process the webhook based on environment
30  if (environment === 'TEST') {
31    console.log('TEST webhook received:', req.body);
32    // Handle test webhook differently...
33  } else {
34    console.log('LIVE webhook received:', req.body);
35    // Handle production webhook...
36  }
37  
38  res.status(200).send('Webhook received');
39});
40
41// Verify the signature
42function verifySignature(payload, signature, secret) {
43  // Decode the webhook secret from base64
44  const secretBytes = Buffer.from(secret, 'base64');
45  
46  // Create HMAC with your webhook secret
47  const hmac = crypto.createHmac('sha256', secretBytes);
48  
49  // Update with the payload
50  hmac.update(payload);
51  
52  // Get the computed signature
53  const computedSignature = hmac.digest('hex');
54  
55  // Compare signatures
56  return crypto.timingSafeEqual(
57    Buffer.from(signature),
58    Buffer.from(computedSignature)
59  );
60}
61
62app.listen(3000, () => {
63  console.log('Server listening on port 3000');
64});

Webhook Retries

If your endpoint returns a non-2xx response or times out, BessaPay will retry the webhook using an exponential backoff strategy:

  • 1st retry: 5 minutes after the original attempt
  • 2nd retry: 15 minutes after the 1st retry
  • 3rd retry: 45 minutes after the 2nd retry
  • And so on, up to a maximum of 8 attempts

After all retries are exhausted, the webhook will be marked as failed and you'll need to use the API to check the transaction status.

Webhook Best Practices

  • Always verify the webhook signature
  • Process webhooks asynchronously to respond quickly
  • Use queuing if you need to process webhooks sequentially
  • Implement idempotency to handle duplicate webhook deliveries
  • Do not use webhooks for critical business logic without a fallback
  • Consider logging all received webhooks for debugging
  • Set appropriate timeouts in your webhook handler
  • Use separate endpoints for TEST and LIVE webhooks when possible
  • Always check the environment field in the webhook payload
  • Store TEST and LIVE webhook secrets securely and separately

Environment Handling Example

A common pattern is to route TEST and LIVE webhooks differently:

Webhook Environment Routing
1// In your webhook handler
2function processWebhook(payload) {
3  const { event, environment } = payload;
4  
5  // Log environment for debugging
6  console.log(`Processing ${event} event from ${environment} environment`);
7  
8  if (environment === 'TEST') {
9    // Send to test database or test processing queue
10    testProcessor.process(payload);
11  } else {
12    // Send to production database or live processing queue
13    liveProcessor.process(payload);
14  }
15}