Adding a payment gateway
Add a new payment method - config entry, gateway class, webhook and a test checklist.
A gateway is one PHP class plus one entry in config/payments.php. The admin Payments page, the checkout list, the return and webhook routes and the logging are generated for you.
1. Declare it in config/payments.php#
'acmepay' => [
'name' => 'AcmePay',
'class' => \App\Payments\Gateways\AcmePayGateway::class,
'fields' => [
'api_key' => ['type' => 'text', 'label' => 'API key'],
'secret' => ['type' => 'password', 'label' => 'Secret'],
'sandbox' => ['type' => 'toggle', 'label' => 'Sandbox mode'],
],
],
Field types: text, password (stored encrypted), textarea, toggle. For offline methods add 'offline' => true. The array key (acmepay) is the gateway key: it appears in the webhook URL /payments/acmepay/webhook and in settings keys payment.acmepay.*.
2. Write the class#
Extend App\Payments\AbstractGateway (implements Contracts\PaymentGateway). Use an existing gateway such as MollieGateway or StripeGateway as the working template.
| Method | Responsibility |
|---|---|
purchase(Order $order, Request $request): GatewayResponse |
Start the payment: call the provider, record a pending transaction (recordTransaction), and return a redirect, client-side payload or completed response. |
complete(Order $order, Request $request): bool |
Handle the customer's return: verify with the provider and settle the order. |
webhook(Request $request): Response |
Handle provider notifications; verify the signature, then settle or fail the order. Answer 2xx for handled, duplicate and unknown events, 400/401 for bad signatures, 500 to ask for a retry. |
supportsRefunds() and refund(Transaction, float $amount, ?string $reason) |
Optional refunds; the amount is in the transaction currency. |
supportedCurrencies() |
Currency codes the gateway accepts (the gateway is hidden for others). |
The base class provides the helpers you should use instead of reinventing them: credential() and isSandbox() for settings, amount(), toMinor(), fromMinor() and formatAmount() for amounts (including zero- and three-decimal currencies), return/cancel/webhook URL builders, recordTransaction() (idempotent), settlePaid() and settleFailed() (idempotent, row-locked) and logging to the payments channel with secrets scrubbed.
3. Security rules to follow#
- Mark an order paid only after verifying with the provider (API call or signature) that the payment exists, references this order, and that amount and currency are at least what the order requires.
- Never trust query parameters on the return URL.
- A stranger visiting a return URL must not be able to fail or cancel an order; fail only on a provider-reported failure for the order's owner.
- If the provider returns by cross-site POST, the route must be stateless (the framework routes
payment.return.postalready are) so the customer's session is not replaced. - Make webhooks idempotent: the same event delivered twice must not create two transactions.
4. Routes and views you get for free#
routes/payment.php defines payment.return, payment.return.post, payment.webhook and payment.redirect for every gateway. For client-side gateways (a pop-up script), add a view in resources/views/front/checkout/gateways/ that defines window.SelloraGateways.{key}(client, done, fail); see razorpay.blade.php.
5. Test it#
- Unit-test purchase, return, webhook (bad signature, tampered amount, wrong currency, foreign reference, duplicate delivery) and refund with mocked HTTP.
tests/Payments/contains a plain-PHP harness with a test file per gateway you can copy. - Then test in the provider's sandbox with a real browser flow: pay, cancel, fail, refund. The README of every release should say plainly which gateways were sandbox-tested.