Webhooks & Callbacks
Receive real-time notifications about payment status changes through secure webhooks.
Overview
Webhooks allow LakiPay to send real-time notifications to your application when payment events occur. This enables you to update your system automatically without polling the API.
DEPOSIT, WITHDRAWAL
RSA-2048 signature verification
Webhook Events
Event Types
Triggered when a payment is received or its status changes
Triggered when a withdrawal is processed or its status changes
Webhook Payload Structure
{
"event": "DEPOSIT",
"transaction_id": "TXN-123456789",
"reference": "ORDER-12345",
"amount": 100.00,
"currency": "ETB",
"status": "SUCCESS",
"medium": "MPESA",
"timestamp": "2024-01-15T10:30:00Z",
"signature": "base64-encoded-signature"
}Status Values
Payment completed successfully
Payment failed
Payment is being processed
Payment was cancelled
Signature Verification
Security Requirement
Always verify webhook signatures to ensure requests are from LakiPay. Never process webhooks without signature verification.
RSA-2048 Encryption
LakiPay uses RSA-2048 encryption for webhook signatures. The signature is included in the webhook payload and must be verified using our public key.
Signature Algorithm: RSA-2048 Signature Format: Base64 encoded Public Key: Available in your dashboard
Verification Process
- 1Extract the signature from the webhook payload
- 2Create a canonical string from the payload (excluding the signature field)
- 3Decode the base64 signature
- 4Verify using RSA-2048 public key
- 5Process the webhook only if verification succeeds
Canonical String Creation
The canonical string is created by:
- 1. Sort all fields alphabetically by key
- 2. Concatenate key-value pairs: key=value
- 3. Join with & separator
- 4. Exclude the signature field
Example: amount=100.00¤cy=ETB&event=DEPOSIT& reference=ORDER-12345&status=SUCCESS& timestamp=2024-01-15T10:30:00Z& transaction_id=TXN-123456789
Verification Examples
Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signature, publicKey) {
// Create canonical string
const canonical = Object.keys(payload)
.filter(key => key !== 'signature')
.sort()
.map(key => `${key}=${payload[key]}`)
.join('&');
// Verify signature
const verify = crypto.createVerify('RSA-SHA256');
verify.update(canonical);
return verify.verify(publicKey, signature, 'base64');
}Python
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import base64
def verify_webhook(payload, signature, public_key):
# Create canonical string
canonical = '&'.join(
f"{k}={v}" for k, v in sorted(
{k: v for k, v in payload.items() if k != 'signature'}.items()
)
)
# Verify signature
signature_bytes = base64.b64decode(signature)
public_key.verify(
signature_bytes,
canonical.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return TrueBest Practices
Always Verify Signatures
Never process webhooks without verifying the signature first
Use HTTPS
Always use HTTPS endpoints for webhook URLs
Idempotency
Handle duplicate webhooks using transaction_id as idempotency key
Timeout Handling
Respond quickly (within 5 seconds) to webhook requests
Logging
Log all webhook events for debugging and auditing
Error Handling
Return appropriate HTTP status codes (200 for success, 4xx/5xx for errors)