PHP API Example
Use 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…
PHP Integration
Use 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. |
1. config.php
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]
);
2. jjuma-client.php
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;
}
}
3. pay.php
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;
4. success.php
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>
5. cancel.php
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>
6. webhook.php
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.
Testing the integration
- Use a verified test merchant and a public HTTPS webhook URL.
- Create a test payment from
pay.phpand confirm the customer lands on the hosted checkout page. - Confirm the browser returns to
success.phporcancel.php. - Verify the webhook reaches
webhook.phpwith a valid signature. - Send the same webhook again and confirm your duplicate protection ignores it.
- Check that orders, subscriptions, packages, donations, or balances update only after the server confirms the payment.