Webhooks
Instead of polling transactions, configure a webhook URL and the API will POST you a signed event whenever something happens — an STK push completes, a checkout invoice is paid, a B2C payout settles, and so on.
Configuration
Webhooks are configured per company: an HTTPS URL and a secret used to sign every delivery. Set these during onboarding or from your merchant dashboard. Deliveries stop if the webhook is marked inactive.
Delivery format
Events are sent as POST requests with a JSON body:
{
"id": "0b0e9a3c-8f1d-4a7b-b1a2-3c4d5e6f7a8b",
"eventType": "transaction.completed",
"timestamp": "2026-07-18T09:15:00.000Z",
"data": {
"transactionId": "a3f1c2d4-...",
"status": "complete",
"amount": 1500,
"accountReference": "INV-1001",
"externalReference": "your-transaction-id"
},
"metadata": {}
}
Headers on every delivery:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | The event type, e.g. transaction.completed |
X-Webhook-Signature | sha512=<hex HMAC of the raw body> |
User-Agent | TransService-Webhook/1.0 |
Deliveries time out after 30 seconds and redirects are not followed — respond with a 2xx directly from the configured URL.
Verifying signatures
The signature is an HMAC-SHA512 of the raw request body, keyed with your webhook secret, hex-encoded and prefixed with sha512=. Always verify it before trusting a delivery — and compute it over the raw body string, not a re-serialized object.
// Express example
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post(
'/webhooks/transactions',
express.raw({ type: 'application/json' }), // keep the raw body!
(req, res) => {
const received = (req.get('X-Webhook-Signature') || '').replace(/^sha512=/, '');
const expected = crypto
.createHmac('sha512', process.env.WEBHOOK_SECRET)
.update(req.body, 'utf8')
.digest('hex');
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body);
// handle event.eventType / event.data ...
res.sendStatus(200);
},
);
Testing your endpoint
Trigger a test delivery to your configured URL any time:
curl -X POST https://sandbox.merchants.taifapay.africa/v1/webhooks/test \
-H "Authorization: Bearer $ACCESS_TOKEN"
It sends a webhook.test event through the exact same signing and delivery path as real events and tells you whether the delivery succeeded.
Handling tips
- Respond fast. Acknowledge with
200immediately and process asynchronously; slow handlers risk the 30-second timeout. - Be idempotent. Use the event
id(ordata.transactionId+eventType) to de-duplicate — treat webhooks as at-least-once. - Don't rely on order. If in doubt about current state, fetch
GET /v1/transactions/{id}— the API is the source of truth.