API documentation

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

EventWhen
sms.sentMessage accepted by the network
sms.deliveredHandset confirmed delivery (DLR)
sms.failedDelivery failed (reason in payload)
otp.generated / otp.verified / otp.expiredOTP lifecycle
ussd.session.start / ussd.session.endUSSD sessions on your service
wallet.credit / wallet.debit / wallet.low_balanceBalance changes and low-balance alerts
sender_id.approved / sender_id.rejectedSender ID review outcome
api_key.created / api_key.revokedKey lifecycle

Request format

Each delivery is an HTTPS POST with a JSON body and these headers:

X-Webhook-EventEvent name, e.g. sms.delivered
X-Webhook-TimestampISO-8601 time the event was sent
X-Webhook-Signaturehex( HMAC-SHA256( secret, raw request body ) )
example 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).

Express
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();
});
PHP
$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.