Webhooks
SwiftWallet POSTs JSON to your URL when a transaction reaches a final status and when a Pay-In (C2B) payment arrives. Every delivery is signed. Verify the signature, then confirm the status with GET /v3/transactions/ before fulfilling an order.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-SwiftWallet-Signature | Lowercase hex HMAC-SHA256 of the exact raw request body. No "sha256=" prefix is sent. |
X-SwiftWallet-Event | Event name, for example transaction.completed or c2b.payment.received. |
X-SwiftWallet-Delivery | Delivery id. Stays the same across retries of one delivery; use it to deduplicate. |
User-Agent | SwiftWallet-Webhook/1.0 for transaction callbacks, SwiftWallet-C2B-Notify/1.0 for C2B notifications. |
Verifying the signature
Compute HMAC-SHA256 over the raw request body (before any JSON parsing) with the signing secret, and compare in constant time.
// PHP
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('SWIFTWALLET_WEBHOOK_SECRET'));
$received = $_SERVER['HTTP_X_SWIFTWALLET_SIGNATURE'] ?? '';
if (! hash_equals($expected, strtolower($received))) {
http_response_code(401);
exit;
}// Node.js (Express): app.post('/hooks/swiftwallet', express.raw({ type: 'application/json' }), handler)
const crypto = require('crypto');
function handler(req, res) {
const expected = crypto.createHmac('sha256', process.env.SWIFTWALLET_WEBHOOK_SECRET).update(req.body).digest('hex');
const received = Buffer.from(String(req.get('X-SwiftWallet-Signature') || ''), 'utf8');
const ok = received.length === expected.length && crypto.timingSafeEqual(received, Buffer.from(expected, 'utf8'));
if (!ok) return res.sendStatus(401);
res.json({ status: 'ok' });
}Events
transaction.{status}
Status values: completed, failed, cancelled, reversed .
Signing secret: The webhook secret (whsec_...) of the API key that created the transaction.
Delivered to: The request's callback_url (user_callback_url for wallet deposits), otherwise the key's webhook URL.
Collections (STK, Lipa na M-Pesa, wallet deposit)
success status transaction_id reference external_reference livemode timestamp checkout_request_id merchant_request_id service_fee result.ResultCode result.ResultDesc result.MpesaReceiptNumber result.Amount result.Phone channel_info.channel_type channel_info.channel_name channel_info.routing reference_number (wallet deposits) wallet_type (wallet deposits)
Payouts (B2C, B2Pochi, B2B, reversals)
success status transaction_id reference external_reference livemode timestamp originatorConversationID conversationID transactionID service_fee withdrawal_fee result.ResultCode result.ResultDesc result.TransactionAmount result.TransactionReceipt result.RecipientIsRegistered result.ReceiverPublicName result.ReceiverPartyPublicName (B2B) transaction_info.phone_number transaction_info.party_b transaction_info.amount transaction_info.currency transaction_info.transaction_type (B2C, B2Pochi, B2B, Reversal) payer_name original_transaction_id (reversals)
c2b.payment.received
Signing secret: The forwarding secret of the Pay-In (C2B) account, shown on its Pay-In page.
Delivered to: The account's verified HTTPS notification URL.
C2B pay-in
event account_code trans_id trans_type trans_time amount business_shortcode bill_ref_number msisdn phone_number msisdn_hash first_name middle_name last_name credited payment_id timestamp
Delivery and retries
- Respond with any HTTP 2xx within 10 seconds. Redirects are not followed and count as a failure.
- Up to 6 attempts in total. Retries wait 30 s, 2 min, 10 min, 30 min, 2 h after each failed attempt.
- URLs must be public HTTPS. A URL that resolves to a private or reserved address is refused when you submit it and again at delivery time.
- Deduplicate on
X-SwiftWallet-Delivery(and ontrans_idfor C2B). Retries resend the identical body, includingtimestamp, so a freshness check must allow for the whole retry window (about 163 minutes). - Payloads never include M-Pesa float or utility balances.