JJuma Global logo Developer Docs
JJuma Pay Documentation

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

JSON
{
  "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-Timestamp
  • X-Jjuma-Event
  • X-Jjuma-Delivery-Id
Node.js
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'));
PHP
$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; }
Python
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.