WHBMP

Building a payment gateway plugin

Oct 25, 2026

The short way: a hosted payment page

If your gateway sends the client to its own page and tells you afterwards (Stripe Checkout, Mollie, PayPal Orders), extend App\Plugins\Contracts\AbstractCheckoutGateway instead. Core then checks the invoice and amount, answers the webhook route, handles the client's return, checks what was paid against the invoice and books it once. You write only:

  • fields(), testConnection() and refund()
  • createCheckout($invoice, $successUrl, $cancelUrl): create the page and return ['success' => true, 'redirect_url' => …]
  • parseWebhook($body, $headers): verify the signature and return an event, or null to refuse the call
  • fetchPayment($reference): ask the gateway about a payment when the client returns, and return an event

An event is an array with a type (paid, pending, failed, refunded, dispute, invalid or ignored) and the fields the class comment of AbstractCheckoutGateway lists. The Stripe plugin is about 340 lines this way.

The long way: everything yourself

What core does for you

A gateway plugin is one class that extends App\Plugins\Contracts\AbstractGatewayModule. Core already provides:

  • The settings screen. List your credentials in fields() (label, type, placeholder, optional, help). They are saved encrypted and read back with $this->credential('key'). Add a static credentialErrors(array $credentials) to refuse a wrong value when it is saved.
  • The Test Connection button. It calls your testConnection(); return ['success' => bool, 'message' => string].
  • The Pay button. It calls charge($amount, $currency, $details) with the invoice amount worked out on the server. Return ['success' => false, 'message' => …] to show an error. For a hosted payment page return ['success' => true, 'redirect_url' => …]; the client is sent there and the invoice stays unpaid until you confirm it.
  • The Refund button. When the plugin implements LiveGatewayModuleInterface, "Original gateway" refunds call your refund($paymentReference, $amount).
  • A public webhook address /webhooks/payments/{adapter} (no session, no CSRF), shown on the settings screen. It calls your handleWebhook($rawBody, $headers); verify the signature yourself and return ['status' => 200, 'message' => 'ok'].
  • The return from the payment page. The client lands on confirmReturn($invoice, $reference); ask the gateway whether it was paid, never trust the request.
  • The books. After the gateway confirms, call App\Support\GatewayPayments::recordPayment($invoice, $gatewayName, $reference, $amount). It books the payment once per reference, refuses a surplus and returns paid, duplicate or surplus. GatewayPayments::recordRefund() does the same for a refund made at the gateway.
  • Amounts. App\Support\MinorUnits::toMinor() and fromMinor() convert to and from the smallest currency unit (yen, dinar and the rest).
  • Logs. Calls through the module appear in System Logs automatically; $this->log() adds a line for a webhook.

Optional extras for a checkout gateway

  • Sandbox box. Add public static function supportsTestMode(): bool { return true; } and the gateway's card shows a test (sandbox) tick box. Read it with $this->testMode().
  • Webhook address text. $this->webhookUrl() is the address core serves. A static webhookHelp() returning '' hides the address box (the gateway is told it with every payment, or needs none); returning text replaces the default help.
  • A form to post. When the gateway's page must be sent a form (PayU, Authorize.Net), createCheckout() can return ['success' => true, 'redirect_post' => ['url' => …, 'fields' => […]]] instead of redirect_url; core shows a page that posts it at once.
  • A reply word or a redirect from the webhook. Override handleWebhook() and add 'body' => 'success' (the exact reply the gateway wants) or 'redirect' => $url (the client's own browser came through the webhook address, as with PayU) to what the parent returns.
  • Carrying the invoice through a payment. App\Support\GatewayReference::make($invoice, $minor) makes a reference that names the invoice, client and amount; parse() reads it back, so what comes back from the gateway is checked against what was asked for. remember() and recall() keep a gateway's own id for a reference until the client returns.

Saving payment methods for automatic payments

Implement App\Plugins\Contracts\SavesPaymentMethodsInterface (startSetup, completeSetup, chargeSaved, forgetMethod) and the gateway appears under the client's Payment Methods and is charged by the daily run (Settings > Automation). startSetup() answers with where to send the client, adding &session_id=<your reference> to the return address it was given; completeSetup() reads the saved method back from the gateway. chargeSaved() returns the gateway's payment reference, the amount and the currency; core checks them against the invoice and books the payment. See Automatic payments; the Stripe plugin is the worked example.

What your plugin adds

Only the gateway's own API calls and its signature check. The Stripe plugin (plugins/stripe) is a complete example of every part above.

Related

Powered by WHBMP