BoomPay/docs

OpenCart

A catalog-side payment extension for OpenCart 4.x: confirm-and-redirect on the storefront, a signed callback, and a deliberately summarized admin settings screen.

PHPCatalog payment extensionOpenCart 4.x

OpenCart’s checkout asks the selected payment method for a confirm-step template, then expects an AJAX confirm call to either place the order and hand back a redirect, or hand back an error. That maps directly onto BoomPay: confirm the order in OpenCart, create the intent, and redirect to payment.link.

!

OpenCart’s exact extension folder depth has shifted between 3.x and 4.x, and again depending on whether you’re packaging for the Marketplace (with an install.json manifest) or installing files directly. The controller below uses OC4’s namespaced class style and the file paths shown — if you’re packaging for marketplace distribution, check the current install.json docs for the wrapping folder structure; the logic inside each file doesn’t change.

Storefront files

catalog/controller/extension/boompay/payment/boompay.php
<?php
namespace Opencart\Catalog\Controller\Extension\Boompay\Payment;

class Boompay extends \Opencart\System\Engine\Controller
{
    public function index(): string
    {
        $this->load->language('extension/boompay/payment/boompay');

        $data['action'] = $this->url->link('extension/boompay/payment/boompay.confirm', '', true);
        $data['text_redirect'] = $this->language->get('text_redirect');
        $data['button_confirm'] = $this->language->get('button_confirm');

        return $this->load->view('extension/boompay/payment/boompay', $data);
    }

    public function confirm(): void
    {
        $this->load->language('extension/boompay/payment/boompay');
        $json = [];

        if (empty($this->session->data['order_id'])) {
            $json['error'] = $this->language->get('error_order');
        }

        if (!$json) {
            $this->load->model('checkout/order');
            $order_info = $this->model_checkout_order->getOrder($this->session->data['order_id']);

            try {
                $intent = $this->createIntent($order_info);
                $json['redirect'] = $intent['link'];
            } catch (\Exception $e) {
                $json['error'] = $e->getMessage();
            }
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    public function callback(): void
    {
        $this->load->model('checkout/order');

        $order_id  = (int) ($this->request->get['order_id'] ?? 0);
        $intent_id = (string) ($this->request->get['paymentIntentId'] ?? '');
        $signature = (string) ($this->request->get['X-Boom-Signature'] ?? '');
        $status    = (string) ($this->request->get['status'] ?? '');

        if (!$this->verifySignature($intent_id, $signature)) {
            $this->response->redirect($this->url->link('checkout/cart'));
            return;
        }

        $payment = $this->getPayment($intent_id);

        if ('success' === $status && !empty($payment['paidAt'])) {
            $this->model_checkout_order->addHistory($order_id, (int) $this->config->get('payment_boompay_complete_status_id'));
            $this->response->redirect($this->url->link('checkout/success'));
        } else {
            $this->model_checkout_order->addHistory($order_id, (int) $this->config->get('payment_boompay_failed_status_id'));
            $this->response->redirect($this->url->link('checkout/cart'));
        }
    }

    private function createIntent(array $order_info): array
    {
        return $this->request('POST', '/v1/boompay/paymentIntent', [
            'amount'     => (float) $order_info['total'],
            'successUrl' => $this->url->link('extension/boompay/payment/boompay.callback', 'order_id=' . $order_info['order_id'] . '&status=success', true),
            'failureUrl' => $this->url->link('extension/boompay/payment/boompay.callback', 'order_id=' . $order_info['order_id'] . '&status=failure', true),
            'label'      => 'Order #' . $order_info['order_id'],
            'metadata'   => ['order_id' => $order_info['order_id']],
        ]);
    }

    private function getPayment(string $id): array
    {
        return $this->request('GET', '/v1/boompay/paymentIntent/' . rawurlencode($id));
    }

    private function verifySignature(string $payment_intent_id, string $signature): bool
    {
        $expected = base64_encode(hash_hmac('sha1', $payment_intent_id, $this->apiKey(), true));
        return hash_equals($expected, str_replace(' ', '+', $signature));
    }

    private function apiKey(): string
    {
        return (bool) $this->config->get('payment_boompay_sandbox')
            ? (string) $this->config->get('payment_boompay_sandbox_api_key')
            : (string) $this->config->get('payment_boompay_api_key');
    }

    private function request(string $method, string $path, array $body = null): array
    {
        $sandbox = (bool) $this->config->get('payment_boompay_sandbox');
        $base = $sandbox ? 'https://sapi.boom.market' : 'https://api.boom.market';

        $ch = curl_init($base . $path);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['x-api-key: ' . $this->apiKey(), 'Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        if ('POST' === $method) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        }

        $response = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        $data = json_decode((string) $response, true) ?? [];

        if ($status < 200 || $status >= 300) {
            throw new \Exception('BoomPay API error (' . $status . '): ' . ($data['message'] ?? 'unknown'));
        }

        return $data;
    }
}
catalog/view/template/extension/boompay/payment/boompay.twig
<div id="boompay-payment">
  <p>{{ text_redirect }}</p>
  <button type="button" id="button-confirm" class="btn btn-primary">{{ button_confirm }}</button>
</div>
<script type="text/javascript">
document.getElementById('button-confirm').addEventListener('click', function () {
  fetch('{{ action }}', { method: 'POST' })
    .then(function (res) { return res.json(); })
    .then(function (json) {
      if (json.redirect) {
        window.location = json.redirect;
      } else if (json.error) {
        alert(json.error);
      }
    });
});
</script>
catalog/language/en-gb/extension/boompay/payment/boompay.php
<?php
$_['text_redirect'] = 'You will be redirected to BoomPay to approve payment from your Boom wallet.';
$_['button_confirm'] = 'Confirm Order';
$_['error_order'] = 'Your order could not be found.';

Admin settings (scaffolding)

The admin side is the boilerplate every OpenCart payment extension needs and doesn’t touch BoomPay directly, so it’s summarized rather than fully reproduced here: a settings controller and Twig template under admin/controller and admin/view/template exposing four fields bound to setting.boompay.* — status (enabled toggle), sandbox mode, live API key, and sandbox API key — plus matching entries in admin/language/en-gb/extension/boompay/payment/boompay.php. These follow the identical pattern OpenCart uses for every other payment extension’s settings screen.

Install

Place the files at the paths shown above relative to your store’s upload/ root (or package them with an install.json for Extension Installer), then enable the extension from Extensions → Payments in the admin and fill in your sandbox API key.

Testing in sandbox

Complete a test checkout choosing BoomPay at the payment step, click Confirm Order, and confirm you’re redirected to a sapi.boom.market page. Approve the test payment and check the order history shows the status configured for payment_boompay_complete_status_id. Cancel a second test payment and confirm the order moves to the failed status instead.

Go-live checklist

  • Turn off sandbox mode and set a live API key only after a full test order has completed.
  • Pick real order statuses for the complete/failed config keys — they default to nothing meaningful until you choose them on the settings screen.
  • Convert your store’s currency total to a BMC amount before calling createIntent if you don’t price directly in BMC.