Webhooks
We POST events to your URL as they happen — delivery reports, OTP verifications, wallet changes — so you never have to poll.
Setting up
Email [email protected] (or use the contact form) with your HTTPS endpoint URL and the events you want. We configure it within one working day and send you a signing secret. Self-service webhook management is coming to the dashboard.
Events
| Event | When |
|---|---|
sms.sent | Message accepted by the network |
sms.delivered | Handset confirmed delivery (DLR) |
sms.failed | Delivery failed (reason in payload) |
otp.generated / otp.verified / otp.expired | OTP lifecycle |
ussd.session.start / ussd.session.end | USSD sessions on your service |
wallet.credit / wallet.debit / wallet.low_balance | Balance changes and low-balance alerts |
sender_id.approved / sender_id.rejected | Sender ID review outcome |
api_key.created / api_key.revoked | Key lifecycle |
Request format
Each delivery is an HTTPS POST with a JSON body and these headers:
X-Webhook-Event | Event name, e.g. sms.delivered |
X-Webhook-Timestamp | ISO-8601 time the event was sent |
X-Webhook-Signature | hex( HMAC-SHA256( secret, raw request body ) ) |
{
"event": "sms.delivered",
"event_id": "sms_8841",
"timestamp": "2026-10-09T09:14:05.000Z",
"data": {
"message_id": "msg_7f3a2c1e",
"recipient": "+233241234567",
"sender_id": "AFIAFASHION",
"status": "delivered",
"cost": 0.04,
"delivered_at": "2026-10-09T09:14:05.000Z"
}
}
Verifying the signature
Always verify before trusting a payload. Compute the HMAC over the raw body bytes (not a re-serialised object).
import crypto from 'node:crypto';
app.post('/webhooks/tian', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', process.env.TIAN_WEBHOOK_SECRET).update(req.body).digest('hex');
const given = req.get('X-Webhook-Signature') || '';
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// ...handle event.event / event.data...
res.status(200).end();
});
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('TIAN_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
http_response_code(200);
Retries
Respond with any 2xx within 10 seconds. Anything else (or a timeout) is retried with exponential back-off, up to the retry count configured for your endpoint (default 3). Deliveries are logged so we can replay missed events on request. Make your handler idempotent using event_id.