Payment Module Development
Payment modules extend ModuleBase, appear at checkout, and optionally
redirect to a PSP before the order is created.
Core properties
| Property | Purpose |
|---|---|
$isPayment = true | Registers as a checkout payment method |
$paymentMethodId | Stored in orders.payment_method (must match radio value) |
$paymentMethodLabel | Admin / checkout label |
$paysBeforeOrder | true for virtual POS — pay before Order::placePending() |
Display hooks
order_payment— radio / UI on checkout (required for selection)order_confirmation— post-order instructions (e.g. bankwire IBAN)- For
paysBeforeOrder, also definegetPaymentPageUrl()and a$routesfront page
Full list: Hook List. Base guide: Create New Module.
processPayment(array $order)
Called after the order is saved for post-order methods (bank transfer, COD-style).
Return ['success' => bool, 'redirect' => string, 'message' => string].
A non-empty redirect sends the customer to that URL.
Callbacks & CSRF exemptions
Register PSP callbacks in $apiActions, e.g.
'callback' => 'api/callback.php', and expose them via
/api/module.php?m={name}&action=callback.
CSRF-exempt module actions (bank POST back):
callback, notify, webhook,
ipn, return, 3d-return / 3dreturn.
Still verify signatures / hashes from the PSP — exemption only skips session CSRF.
Reference modules
bankwire— post-order bank transfer + confirmation hooksanalpos—paysBeforeOrder+ front payment route- PayTR / ParamPOS-style modules — token + iframe / 3D Secure + callback verify
Security:
Never trust amounts or status from the browser.
Verify PSP signatures on every callback; mark paid only after a successful verify.
Stay PHP 7.4 compatible (no
match, named arguments, or union types).
PHP
<?php
if (!defined('IN_SCRIPT') && !defined('IN_ADMIN')) {
exit;
}
class DemoPayModule extends ModuleBase
{
public string $name = 'demo-pay';
public string $version = '1.0.0';
public string $displayName = 'Demo Pay';
public string $description = 'Sample payment module';
public bool $isPayment = true;
public string $paymentMethodId = 'demo_pay';
public string $paymentMethodLabel = 'Demo Pay';
public bool $paysBeforeOrder = false;
public array $displayHooks = [
'order_payment' => 'Checkout payment option',
'order_confirmation' => 'Thank-you page instructions',
];
public array $defaultDisplayHooks = [
'order_payment',
'order_confirmation',
];
public array $apiActions = [
'callback' => 'api/callback.php',
];
public function processPayment(array $order): array
{
// Post-order flow (bankwire-style). Redirect only if needed.
return [
'success' => true,
'redirect' => '',
'message' => '',
];
}
public function renderDisplayHook(string $hook, array $context = [])
{
if ($hook === 'order_payment') {
return $this->renderFrontTemplate('order_payment', [
'paymentMethodId' => $this->paymentMethodId,
'label' => $this->paymentMethodLabel,
'formData' => isset($context['formData']) ? $context['formData'] : [],
]);
}
if ($hook === 'order_confirmation') {
return $this->renderFrontTemplate('order_confirmation', [
'order' => isset($context['order']) ? $context['order'] : [],
]);
}
return null;
}
}
callback.php — signature verify sketch
<?php
// modules/demo-pay/api/callback.php — verify PSP signature sketch
if (!defined('IN_SCRIPT')) {
exit;
}
header('Content-Type: application/json; charset=utf-8');
$merchantKey = (string) Settings::get('DEMO_PAY_MERCHANT_KEY');
$payload = file_get_contents('php://input');
$data = json_decode((string) $payload, true);
if (!is_array($data)) {
$data = $_POST;
}
$provided = isset($data['hash']) ? (string) $data['hash'] : '';
$orderId = isset($data['merchant_oid']) ? (string) $data['merchant_oid'] : '';
$status = isset($data['status']) ? (string) $data['status'] : '';
$total = isset($data['total_amount']) ? (string) $data['total_amount'] : '';
$expected = base64_encode(hash_hmac(
'sha256',
$orderId . $merchantKey . $status . $total,
$merchantKey,
true
));
if ($provided === '' || !hash_equals($expected, $provided)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Invalid signature']);
exit;
}
// Never trust client amount alone — load order and compare server-side totals here.
if ($status !== 'success') {
echo json_encode(['success' => false, 'message' => 'Payment not successful']);
exit;
}
echo json_encode(['success' => true]);