Skip to main content

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:

HeaderValue
Content-Typeapplication/json
X-Webhook-EventThe event type, e.g. transaction.completed
X-Webhook-Signaturesha512=<hex HMAC of the raw body>
User-AgentTransService-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 200 immediately and process asynchronously; slow handlers risk the 30-second timeout.
  • Be idempotent. Use the event id (or data.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.