JJuma Global logo Developer Docs
JJuma Pay Documentation

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.

Recommended flow Create the payment on your server, redirect the customer to the returned payment URL, let the customer return to your success or cancel page, and update your records from the webhook.
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
<?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
<?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
<?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
<?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
<?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
<?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';
Duplicate protection Store the webhook 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

  1. Use a verified test merchant and a public HTTPS webhook URL.
  2. Create a test payment from pay.php and confirm the customer lands on the hosted checkout page.
  3. Confirm the browser returns to success.php or cancel.php.
  4. Verify the webhook reaches webhook.php with a valid signature.
  5. Send the same webhook again and confirm your duplicate protection ignores it.
  6. Check that orders, subscriptions, packages, donations, or balances update only after the server confirms the payment.