Webhooks

Lipachap notifies your server when a payment reaches a final state.

Setup

  1. Open the merchant dashboardWebhooks.
  2. Add an HTTPS endpoint on your platform (e.g. https://api.yourapp.com/webhooks/lipachap).
  3. Save the signing secret — used to verify X-Gateway-Signature.

Delivery

Headers

HeaderDescription
X-Gateway-TimestampUnix epoch seconds when the webhook was signed
X-Gateway-Signaturesha256=<hex> HMAC of {timestamp}.{rawBody}
X-Gateway-NonceUnique id per delivery attempt
X-Request-IdCorrelation 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