Webhooks
Configure up to three destinations in Dashboard > Tools > Webhooks. Only active destinations receive their selected events. Use HTTPS in production and keep…
Webhooks
Configure up to three destinations in Dashboard > Tools > Webhooks. Only active destinations receive their selected events. Use HTTPS in production and keep handlers idempotent.
- Events: payment.started, payment.completed, payment.failed, payment.cancelled, settlement.pending, settlement.available, settlement.completed, withdrawal.paid.
- Failed automatic deliveries are attempted up to three times with a 10-second timeout per attempt.
- Delivery failures do not change a successful payment result; logs and manual retry are available in the dashboard.
- A request-level webhook_url overrides dashboard destinations for that transaction. If omitted, active dashboard webhooks are used. Browser redirect URLs never replace webhook delivery.
Example webhook request
{
"event": "payment.completed",
"event_id": "evt_123",
"delivery_id": "wh_456",
"reference": "REF-12345678",
"amount": 50000,
"currency": "UGX",
"status": "successful"
}
Dashboard-managed deliveries use HMAC SHA256 with the exact raw body. Sign X-Jjuma-Timestamp + "." + raw_body using your owner-only webhook secret.
X-Jjuma-Signature: sha256=<hex digest>(raw hex is also accepted by the verifier)X-Jjuma-TimestampX-Jjuma-EventX-Jjuma-Delivery-Id
const crypto = require('crypto');
const timestamp = req.get('X-Jjuma-Timestamp') || '';
const supplied = (req.get('X-Jjuma-Signature') || '').replace(/^sha256=/i, '');
const expected = crypto.createHmac('sha256', process.env.JJUMA_WEBHOOK_SECRET)
.update(timestamp + '.' + req.rawBody.toString('utf8')).digest('hex');
const valid = supplied.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(supplied, 'hex'), Buffer.from(expected, 'hex'));
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_JJUMA_TIMESTAMP'] ?? '';
$signature = preg_replace('/^sha256=/i', '', $_SERVER['HTTP_X_JJUMA_SIGNATURE'] ?? '');
$expected = hash_hmac('sha256', $timestamp . '.' . $raw, getenv('JJUMA_WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) { http_response_code(401); exit; }
raw = request.get_data()
timestamp = request.headers.get('X-Jjuma-Timestamp', '')
signature = request.headers.get('X-Jjuma-Signature', '').removeprefix('sha256=')
expected = hmac.new(SECRET.encode(), timestamp.encode() + b'.' + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature): abort(401)
Regenerating the secret keeps webhook destinations but invalidates the old secret. Request-level transaction webhook URLs do not use the dashboard signing secret.