Skip to main content
You only teach it how to talk to your provider.
Read Fundamentals first if you haven’t.

Generate it

Drop --webhook if the provider has no webhooks.

Two naming rules

Both are enforced by how discovery works, so they aren’t optional.
1

The module must be named {Something}Gateway

PaymentServiceProvider scans Modules/Gateways/* and derives a key from the name, so AcmeGateway becomes acme.
2

The payment service must be at Modules\{Module}\Services\{Module}PaymentService

And getGatewayName() has to return that same key.
Registration is lazy: nothing is instantiated at boot, so credentials are only decrypted when a payment actually happens. Don’t put API calls in your constructor.

What you implement

BasePaymentService handles every billing flow already. It looks at $paymentData['type'] and routes to the right one.
You write six methods. The generator stubs all of them.
app/Services/AcmeGatewayPaymentService.php

enabled is the on/off switch

Keep it false until the gateway is fully configured. A half-set-up gateway appearing at checkout is worse than it not appearing at all.
The available-gateways list is cached for five minutes, so your settings controller has to call flushPaymentGatewayCache() after saving. The generator does this. Remove it and admins will think saving is broken.

Verification: pick one

Four rules, and none of them are stylistic.
Re-query the provider inside executePayment(). Query strings are attacker-controlled.
Providers retry. handlePaymentSuccess() has to be safe to call again for the same payment, so check the PaymentToken state first.
Not just the status. Confirm what was paid is what you asked for.

Native subscriptions

Optional.
If your provider owns the recurring schedule (Stripe Subscriptions, PayPal Billing Agreements), add a flow handler. Core will use it instead of one-off payments. Each is a should...() predicate plus a handle...() action. Core probes with method_exists() first, so a half-finished handler falls back to the normal flow rather than erroring.
The argument lists are long and positional: handleCheckout() takes 23, handleRenewal() 7, handleInvoicePayment() 6. module:doctor checks yours against those counts.
Declare it explicitly rather than relying on the class name:
Modules/Gateways/BankWireGateway/Services/CheckoutHandler.php is a short readable example: it creates the invoice and waits for proof of payment.

Several checkout options from one module

Optional.
Implement GatewayVariantInterface to appear more than once, like “PayPal” and “PayPal Subscription”.

Test it

Then run every flow in sandbox, because they all go through the same type switch:
1

New purchase

2

Renewal

3

Invoice payment

4

Upgrade

5

Balance top-up

6

Cancel halfway

Confirm no service is created.
7

Pay, then close the browser before the redirect

The webhook should still finish the order.
8

Replay the same webhook twice

Nothing should double-charge or double-provision.
The last two are where real gateways break.

Worth reading

BankWireGateway

Offline payment, no API at all.

CashfreeGateway

Clean HMAC webhook verification.

VippsGateway

Straightforward redirect flow.

StripeGateway

Cards plus native subscriptions.

PayPalGateway

Everything, including variants and disputes.
Two rough edges. PaymentGatewayInterface assumes one flow (create, approve, execute), so refunds and partial captures have no contract and gateways that support them expose their own methods. And BankWireGateway mixes root and app/ layouts, which makes it a good behavioural reference and a poor structural one.