Magento 2 / Adobe Commerce
A standard offsite-redirect payment module: AbstractMethod + OrderPlaceRedirectInterface, a CSRF-exempt callback controller, and the usual invoice-on-payment flow.
This follows the same redirect-method shape Magento has used for offsite gateways since the PayPal Standard era: a payment model that points checkout at a controller instead of processing a card directly, a redirect controller that talks to BoomPay, and a callback controller that invoices the order once the signed return checks out.
Module files
Module declaration
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'BoomPay_Payment',
__DIR__
);
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="BoomPay_Payment" setup_version="1.0.0">
<sequence>
<module name="Magento_Sales"/>
<module name="Magento_Payment"/>
<module name="Magento_Checkout"/>
</sequence>
</module>
</config>
Configuration
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/config.xsd">
<default>
<payment>
<boompay>
<active>0</active>
<model>BoomPay\Payment\Model\BoomPay</model>
<title>Pay with Boomcoin (BoomPay)</title>
<sandbox>1</sandbox>
<allow_multiple_address>1</allow_multiple_address>
<group>offline</group>
</boompay>
</payment>
</default>
</config>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="payment">
<group id="boompay" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1">
<label>BoomPay (Boomcoin)</label>
<field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1">
<label>Title</label>
</field>
<field id="sandbox" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1">
<label>Sandbox mode</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="api_key" translate="label" type="obscure" sortOrder="4" showInDefault="1" showInWebsite="1">
<label>Live API key</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
<field id="sandbox_api_key" translate="label" type="obscure" sortOrder="5" showInDefault="1" showInWebsite="1">
<label>Sandbox API key</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
</group>
</section>
</system>
</config>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd">
<methods>
<method name="boompay">
<allow_multiple_address>1</allow_multiple_address>
</method>
</methods>
</config>
Routing
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="boompay" frontName="boompay">
<module name="BoomPay_Payment"/>
</route>
</router>
</config>
BoomPay client
<?php
namespace BoomPay\Payment\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\HTTP\Client\Curl;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\Exception\LocalizedException;
class Client extends AbstractHelper
{
const SANDBOX_BASE = 'https://sapi.boom.market';
const LIVE_BASE = 'https://api.boom.market';
private $curl;
private $json;
public function __construct(
\Magento\Framework\App\Helper\Context $context,
Curl $curl,
Json $json
) {
parent::__construct($context);
$this->curl = $curl;
$this->json = $json;
}
public function createIntent($apiKey, $sandbox, $amount, $successUrl, $failureUrl, $label, $metadata = [])
{
return $this->request($apiKey, $sandbox, 'POST', '/v1/boompay/paymentIntent', [
'amount' => $amount,
'successUrl' => $successUrl,
'failureUrl' => $failureUrl,
'label' => $label,
'metadata' => $metadata,
]);
}
public function getPayment($apiKey, $sandbox, $id)
{
return $this->request($apiKey, $sandbox, 'GET', '/v1/boompay/paymentIntent/' . rawurlencode($id));
}
/** signature = base64(HMAC-SHA1(apiKey, paymentIntentId)) -- see API Reference > Verifying the return */
public function verifySignature($apiKey, $paymentIntentId, $signature)
{
$expected = base64_encode(hash_hmac('sha1', $paymentIntentId, $apiKey, true));
$received = str_replace(' ', '+', (string) $signature);
return hash_equals($expected, $received);
}
private function request($apiKey, $sandbox, $method, $path, $body = null)
{
$base = $sandbox ? self::SANDBOX_BASE : self::LIVE_BASE;
$this->curl->addHeader('x-api-key', $apiKey);
$this->curl->addHeader('Content-Type', 'application/json');
if ($method === 'POST') {
$this->curl->post($base . $path, $this->json->serialize($body));
} else {
$this->curl->get($base . $path);
}
$status = $this->curl->getStatus();
$data = $this->json->unserialize($this->curl->getBody());
if ($status < 200 || $status >= 300) {
$message = $data['message'] ?? 'Unknown BoomPay API error';
throw new LocalizedException(__('BoomPay API error (%1): %2', $status, $message));
}
return $data;
}
}
Payment method model
<?php
namespace BoomPay\Payment\Model;
use Magento\Payment\Model\Method\AbstractMethod;
use Magento\Payment\Model\Method\OrderPlaceRedirectInterface;
class BoomPay extends AbstractMethod implements OrderPlaceRedirectInterface
{
protected $_code = 'boompay';
protected $_isOffline = false;
protected $_canCapture = false;
protected $_canAuthorize = false;
protected $_canRefund = false;
private $urlBuilder;
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
\Magento\Framework\Api\AttributeValueFactory $customAttributeFactory,
\Magento\Payment\Helper\Data $paymentData,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Payment\Model\Method\Logger $logger,
\Magento\Framework\UrlInterface $urlBuilder,
array $data = []
) {
parent::__construct(
$context, $registry, $extensionFactory, $customAttributeFactory,
$paymentData, $scopeConfig, $logger, null, null, $data
);
$this->urlBuilder = $urlBuilder;
}
public function getOrderPlaceRedirectUrl()
{
return $this->urlBuilder->getUrl('boompay/payment/redirect');
}
public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null)
{
return parent::isAvailable($quote) && (bool) $this->getConfigData('active');
}
}
Controllers
<?php
namespace BoomPay\Payment\Controller\Payment;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Checkout\Model\Session as CheckoutSession;
class Redirect extends Action
{
private $checkoutSession;
private $client;
private $scopeConfig;
private $orderRepository;
public function __construct(
Context $context,
CheckoutSession $checkoutSession,
\BoomPay\Payment\Helper\Client $client,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Sales\Api\OrderRepositoryInterface $orderRepository
) {
parent::__construct($context);
$this->checkoutSession = $checkoutSession;
$this->client = $client;
$this->scopeConfig = $scopeConfig;
$this->orderRepository = $orderRepository;
}
public function execute()
{
$order = $this->checkoutSession->getLastRealOrder();
$sandbox = (bool) $this->scopeConfig->getValue('payment/boompay/sandbox');
$apiKey = $this->scopeConfig->getValue(
$sandbox ? 'payment/boompay/sandbox_api_key' : 'payment/boompay/api_key'
);
$intent = $this->client->createIntent(
$apiKey,
$sandbox,
(float) $order->getGrandTotal(),
$this->_url->getUrl('boompay/payment/callback', ['status' => 'success', 'order_id' => $order->getId()]),
$this->_url->getUrl('boompay/payment/callback', ['status' => 'failure', 'order_id' => $order->getId()]),
sprintf('Order #%s', $order->getIncrementId()),
['order_id' => $order->getId()]
);
$order->getPayment()->setAdditionalInformation('boompay_intent_id', $intent['id']);
$this->orderRepository->save($order);
return $this->resultRedirectFactory->create()->setUrl($intent['link']);
}
}
<?php
namespace BoomPay\Payment\Controller\Payment;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Sales\Model\Order;
class Callback extends Action implements CsrfAwareActionInterface
{
private $orderRepository;
private $client;
private $scopeConfig;
private $invoiceService;
public function __construct(
Context $context,
\Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
\BoomPay\Payment\Helper\Client $client,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Sales\Model\Service\InvoiceService $invoiceService
) {
parent::__construct($context);
$this->orderRepository = $orderRepository;
$this->client = $client;
$this->scopeConfig = $scopeConfig;
$this->invoiceService = $invoiceService;
}
// BoomPay's browser redirect arrives with no Magento form key, so this
// controller has to opt out of the framework's default CSRF check.
public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
{
return null;
}
public function validateForCsrfRequest(RequestInterface $request): ?bool
{
return true;
}
public function execute()
{
$orderId = (int) $this->getRequest()->getParam('order_id');
$intentId = (string) $this->getRequest()->getParam('paymentIntentId');
$signature = (string) $this->getRequest()->getParam('X-Boom-Signature');
$status = (string) $this->getRequest()->getParam('status');
$order = $this->orderRepository->get($orderId);
$sandbox = (bool) $this->scopeConfig->getValue('payment/boompay/sandbox');
$apiKey = $this->scopeConfig->getValue(
$sandbox ? 'payment/boompay/sandbox_api_key' : 'payment/boompay/api_key'
);
if (!$this->client->verifySignature($apiKey, $intentId, $signature)) {
return $this->_redirect('checkout/cart', ['_query' => ['boompay_error' => 'signature']]);
}
$payment = $this->client->getPayment($apiKey, $sandbox, $intentId);
if ($status === 'success' && !empty($payment['paidAt'])) {
$invoice = $this->invoiceService->prepareInvoice($order);
$invoice->register();
$order->addRelatedObject($invoice);
$order->setState(Order::STATE_PROCESSING)->setStatus('processing');
$this->orderRepository->save($order);
return $this->_redirect('checkout/onepage/success');
}
$order->setState(Order::STATE_CANCELED)->setStatus('canceled');
$this->orderRepository->save($order);
return $this->_redirect('checkout/cart');
}
}
The callback controller implements CsrfAwareActionInterface and opts out of form-key validation. This is necessary and correct here — the request arrives from BoomPay’s redirect, not from a form on your own site — but it also means the signature check above it is the only thing standing between this route and a forged request. Don’t relax it.
Install
Place the files under app/code/BoomPay/Payment/ matching the paths above, then from the Magento root:
bin/magento module:enable BoomPay_Payment
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
Configure it under Stores → Configuration → Sales → Payment Methods → BoomPay (Boomcoin).
Testing in sandbox
With sandbox mode enabled and a sandbox API key set, place an order through checkout selecting BoomPay. Confirm you land on a sapi.boom.market page, approve the test payment, and check that the order appears under Sales → Orders as Processing with an invoice attached. Then deliberately let a second test payment expire or cancel it, and confirm the order moves to Canceled rather than sitting in limbo.
Go-live checklist
- Run
setup:di:compileagain after any code change in production mode — Magento will silently serve stale generated code otherwise. - Confirm the live API key is stored via the encrypted backend model (it is, in the config above) so it doesn’t sit in plaintext in
core_config_data. - BoomPay has no FX endpoint — convert your store’s base currency to a BMC amount before calling
createIntentif you don’t price directly in BMC. - Test the cancel/failure path specifically, not just the happy path — offsite redirects are where abandoned-cart orders most often get stuck in the wrong state.