Server requirements
- PHP 7.4 or newer
- PHP cURL and PHP JSON extensions
- HTTPS with a valid SSL certificate
- A public webhook URL
- Access to your merchant database
- PHP 8.1 or newer is recommended
Developer Docs
A complete step by step guide for integrating JJuma payments into a PHP web application with secure checkout redirects, signed webhooks, database updates, duplicate protection, and UGX payment examples.
Keep protected credentials on the PHP server only.
This section shows a complete PHP integration flow that matches the existing Jjuma Pay API rules, webhook headers, and response format.
config.phpjjuma-client.phppay.phpsuccess.phpcancel.phpwebhook.phpUse this guide when your checkout, return pages, and webhook handler are built in PHP. The browser should be redirected to the hosted Jjuma checkout page, while payment confirmation and order updates must happen on the server.
| File | Purpose |
|---|---|
config.php |
Stores API keys, return URLs, webhook URL, and database connection settings. |
jjuma-client.php |
Wraps the JJuma create-payment and verify-payment requests. |
pay.php |
Creates the payment, saves the local record, and redirects the customer to checkout. |
success.php |
Shows the return page and can verify the transaction again from the server. |
cancel.php |
Shows the cancel page and lets the customer retry later. |
webhook.php |
Verifies the webhook signature, prevents duplicates, and updates your database. |
Keep your credentials and merchant URLs on the server only.
<?php
declare(strict_types=1);
define('JJUMA_API_BASE_URL', 'https://api.jjuma.com');
define('JJUMA_PUBLIC_API_KEY', 'bp_live_pub_your_public_key');
define('JJUMA_SECRET_API_KEY', 'YOUR_SECRET_API_KEY');
define('JJUMA_WEBHOOK_SECRET', 'your_webhook_secret');
define('APP_BASE_URL', 'https://merchant.example.com');
define('JJUMA_SUCCESS_URL', APP_BASE_URL . '/success.php');
define('JJUMA_CANCEL_URL', APP_BASE_URL . '/cancel.php');
define('JJUMA_WEBHOOK_URL', APP_BASE_URL . '/webhook.php');
$pdo = new PDO(
'mysql:host=localhost;dbname=merchant_db;charset=utf8mb4',
'db_user',
'db_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
This helper keeps your JJuma calls in one place and reuses the documented auth header and response handling.
<?php
declare(strict_types=1);
final class JjumaClient
{
private string $apiBaseUrl;
private string $publicApiKey;
private string $secretApiKey;
public function __construct(string $apiBaseUrl, string $publicApiKey, string $secretApiKey)
{
$this->apiBaseUrl = $apiBaseUrl;
$this->publicApiKey = $publicApiKey;
$this->secretApiKey = $secretApiKey;
}
public function createPayment(array $payload): array
{
return $this->request('POST', '/api/v1/payments/create', $this->publicApiKey, $payload);
}
public function verifyPayment(string $transactionId): array
{
return $this->request('GET', '/api/v1/payments/verify/' . rawurlencode($transactionId), $this->secretApiKey);
}
private function request(string $method, string $path, string $apiKey, ?array $payload = null): array
{
$ch = curl_init($this->apiBaseUrl . $path);
$headers = [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_SLASHES));
}
$body = curl_exec($ch);
if ($body === false) {
$error = curl_error($ch) ?: 'Unknown cURL error';
curl_close($ch);
throw new RuntimeException($error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($body, true);
if (!is_array($decoded)) {
throw new RuntimeException('JJuma returned invalid JSON.');
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException($decoded['message'] ?? 'JJuma request failed.');
}
return $decoded;
}
}
Create the payment from your server, store the local record, and redirect the customer to the hosted checkout page.
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/jjuma-client.php';
$client = new JjumaClient(JJUMA_API_BASE_URL, JJUMA_PUBLIC_API_KEY, JJUMA_SECRET_API_KEY);
$orderId = trim($_POST['order_id'] ?? '');
$payload = [
'amount' => (int) ($_POST['amount'] ?? 0),
'currency' => 'UGX',
'description' => trim($_POST['description'] ?? 'Order payment'),
'customer_name' => trim($_POST['customer_name'] ?? ''),
'customer_email' => trim($_POST['customer_email'] ?? ''),
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
'redirect_url' => JJUMA_SUCCESS_URL . '?order_id=' . rawurlencode($orderId),
'return_url' => JJUMA_SUCCESS_URL . '?order_id=' . rawurlencode($orderId),
'cancel_redirect_url' => JJUMA_CANCEL_URL . '?order_id=' . rawurlencode($orderId),
'webhook_url' => JJUMA_WEBHOOK_URL,
'metadata' => ['order_id' => $orderId],
'external_order_id' => $orderId,
'idempotency_key' => 'order-' . $orderId,
'provider' => 'jjuma',
];
$response = $client->createPayment($payload);
$paymentUrl = $response['data']['payment_url'] ?? '';
$transactionId = $response['data']['transaction_id'] ?? '';
$reference = $response['data']['reference'] ?? '';
// Save $transactionId, $reference, and a pending status in your merchant database here.
if ($paymentUrl === '') {
http_response_code(500);
exit('Payment URL missing.');
}
header('Location: ' . $paymentUrl);
exit;
Return the customer to your website, then verify the payment again on the server using the stored transaction ID.
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/jjuma-client.php';
$orderId = trim($_GET['order_id'] ?? '');
// Load the local record by $orderId and fetch the saved transaction_id.
$transactionId = 'TXN-A1B2C3D4E5F6';
$client = new JjumaClient(JJUMA_API_BASE_URL, JJUMA_PUBLIC_API_KEY, JJUMA_SECRET_API_KEY);
$verification = $client->verifyPayment($transactionId);
if (($verification['data']['status'] ?? '') === 'successful') {
// Mark the order, subscription, package, donation, or balance as paid.
}
?>
<!doctype html>
<html><body>
<h1>Payment received</h1>
<p>Thanks for your order. We are confirming the payment on the server.</p>
</body></html>
Use the cancel page to show that checkout was closed and that the customer can try again later.
<?php
declare(strict_types=1);
$orderId = trim($_GET['order_id'] ?? '');
// Update the local record as cancelled or left pending, based on your business rules.
?>
<!doctype html>
<html><body>
<h1>Checkout cancelled</h1>
<p>No payment was completed. The customer can return and try again.</p>
</body></html>
Use the webhook to keep your merchant records in sync even if the browser is closed after checkout.
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
$rawBody = file_get_contents('php://input') ?: '';
$timestamp = $_SERVER['HTTP_X_JJUMA_TIMESTAMP'] ?? '';
$signature = preg_replace('/^sha256=/i', '', $_SERVER['HTTP_X_JJUMA_SIGNATURE'] ?? '');
$event = $_SERVER['HTTP_X_JJUMA_EVENT'] ?? '';
$deliveryId = $_SERVER['HTTP_X_JJUMA_DELIVERY_ID'] ?? '';
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, JJUMA_WEBHOOK_SECRET);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($rawBody, true);
if (!is_array($data)) {
http_response_code(400);
exit('Invalid JSON');
}
$eventId = $data['event_id'] ?? '';
$transactionId = $data['transaction_id'] ?? ($data['data']['transaction_id'] ?? '');
$reference = $data['reference'] ?? ($data['data']['reference'] ?? '');
// Skip duplicate event_id, deliveryId, transactionId, or reference values in your database.
// Mark the event as processed before applying business updates.
switch ($event) {
case 'payment.completed':
// Mark orders, subscriptions, packages, donations, or balances as paid.
break;
case 'payment.failed':
case 'payment.cancelled':
// Keep the local record unpaid or cancelled.
break;
case 'settlement.completed':
// If you track wallet balances or settlement status, update them here.
break;
case 'withdrawal.paid':
// Update withdrawal records if your application tracks payouts.
break;
}
http_response_code(200);
echo 'OK';
event_id, delivery_id, transaction_id, or reference in a processed-events table and ignore repeats so the same payment cannot update your order twice.
pay.php and confirm the customer lands on the hosted checkout page.success.php or cancel.php.webhook.php with a valid signature.