Webhooks
Lipachap notifies your server when a payment reaches a final state.
Setup
- Open the merchant dashboard → Webhooks.
- Add an HTTPS endpoint on your platform (e.g.
https://api.yourapp.com/webhooks/lipachap). - Save the signing secret — used to verify
X-Gateway-Signature.
Delivery
- Method:
POSTwithContent-Type: application/json - Retries with backoff on non-2xx responses or timeouts
- Each attempt includes a fresh
X-Gateway-Nonce
Headers
| Header | Description |
|---|---|
X-Gateway-Timestamp | Unix epoch seconds when the webhook was signed |
X-Gateway-Signature | sha256=<hex> HMAC of {timestamp}.{rawBody} |
X-Gateway-Nonce | Unique id per delivery attempt |
X-Request-Id | Correlation id for support |
Payload
{
"transid": "TXN-001",
"reference": "LC-ABC123",
"amount": 5000,
"providerFee": 0,
"platformFee": 0,
"totalFee": 0,
"totalCharged": 5000,
"status": "SUCCESS",
"msisdn": "712345678",
"utilityref": "ORDER-123",
"timestamp": "2026-05-28T10:00:00.000Z",
"resultcode": "000",
"message": "Payment completed successfully"
}
status is SUCCESS or FAILED. Always verify the signature before trusting the body.
Verification
const crypto = require('crypto');
function verifyWebhook(rawBody, timestamp, signature, secret, maxSkewSec = 300) {
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > maxSkewSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = signature.replace(/^sha256=/, '');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
// Express example
app.post('/webhooks/lipachap', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const ok = verifyWebhook(
rawBody,
req.get('X-Gateway-Timestamp'),
req.get('X-Gateway-Signature'),
process.env.LIPACHAP_WEBHOOK_SECRET,
);
if (!ok) return res.sendStatus(401);
const event = JSON.parse(rawBody);
// event.transid, event.status, event.amount, ...
res.sendStatus(200);
});import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public boolean verifyWebhook(String rawBody, String timestamp, String signature, String secret) {
String payload = timestamp + "." + rawBody;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
String received = signature.replace("sha256=", "");
return MessageDigest.isEqual(expected.getBytes(), received.getBytes());
}function verify_lipachap_webhook(string $rawBody, string $timestamp, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$received = preg_replace('/^sha256=/', '', $signature);
return hash_equals($expected, $received);
}
$rawBody = file_get_contents('php://input');
$ok = verify_lipachap_webhook(
$rawBody,
$_SERVER['HTTP_X_GATEWAY_TIMESTAMP'] ?? '',
$_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '',
getenv('LIPACHAP_WEBHOOK_SECRET'),
);import hmac
import hashlib
import time
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str, max_skew: int = 300) -> bool:
if abs(int(time.time()) - int(timestamp)) > max_skew:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
received = signature.removeprefix("sha256=")
return hmac.compare_digest(expected, received)func VerifyWebhook(rawBody []byte, timestamp, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "." + string(rawBody)))
expected := hex.EncodeToString(mac.Sum(nil))
received := strings.TrimPrefix(signature, "sha256=")
return hmac.Equal([]byte(expected), []byte(received))
}Handler checklist
- Return HTTP 200 after persisting idempotently
- Reject requests older than ~5 minutes (timestamp skew)
- Log
transidandreferencefor support - Do not expose the signing secret to clients