> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dezerx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment Gateways

> Take money through a new payment provider. Core already knows how to turn a successful payment into an invoice, a service, a renewal or account credit.

You only teach it how to talk to your provider.

<Note>
  Read [Fundamentals](/extensions/tutorials/fundamentals) first if you haven't.
</Note>

## Generate it

```bash theme={null}
php artisan make:module Acme --type=gateway --webhook
php artisan module:enable AcmeGateway
php artisan module:migrate AcmeGateway
php artisan module:doctor AcmeGateway
```

Drop `--webhook` if the provider has no webhooks.

<Accordion title="What you get" icon="folder-tree">
  ```text theme={null}
  Modules/Gateways/AcmeGateway/
  ├── module.json
  ├── app/
  │   ├── Providers/
  │   ├── Http/Controllers/
  │   │   ├── PaymentController      3 lines, inherits everything
  │   │   └── SettingsController     admin page
  │   ├── Services/
  │   │   └── AcmeGatewayPaymentService   your work goes here
  │   └── Models/AcmeCredential
  └── routes/                        web, api (webhook), settings, payment
  ```
</Accordion>

## Two naming rules

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

<Steps>
  <Step title="The module must be named {Something}Gateway">
    `PaymentServiceProvider` scans `Modules/Gateways/*` and derives a key from the name, so
    `AcmeGateway` becomes `acme`.
  </Step>

  <Step title="The payment service must be at Modules\{Module}\Services\{Module}PaymentService">
    And `getGatewayName()` has to return that same key.
  </Step>
</Steps>

<Info>
  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.
</Info>

## What you implement

`BasePaymentService` handles every billing flow already. It looks at `$paymentData['type']` and routes
to the right one.

<Accordion title="The flows it covers" icon="arrows-split-up-and-left">
  | Type                          | Meaning            |
  | ----------------------------- | ------------------ |
  | `market`                      | New purchase       |
  | `renewal`                     | Renewal            |
  | `invoice`                     | Existing invoice   |
  | `upgrade`, `downgrade`        | Plan change        |
  | `addon`                       | Addon purchase     |
  | `balance`                     | Account credit     |
  | `standalone`                  | Standalone invoice |
  | `configurable_options_change` | Resource change    |
</Accordion>

You write six methods. The generator stubs all of them.

```php app/Services/AcmeGatewayPaymentService.php theme={null}
class AcmeGatewayPaymentService extends BasePaymentService
{
    // Create the payment at the provider. Return a payment id, plus an
    // approval URL if the customer needs to be redirected.
    public function createPayment(float $amount, string $currency, array $metadata = []): array

    // Confirm it actually succeeded. Re-query the provider here.
    public function executePayment(string $paymentId, ?string $payerId = null): bool

    public function getPaymentDetails(string $paymentId): array

    // Null means the token is dead and core should start over.
    public function getApprovalUrl(string $paymentId, array $paymentDetails): ?string

    // 'enabled' decides whether this shows at checkout.
    public function getConfig(): array

    public function validateNotification(array $data): bool

    // 'redirect', 'webhook' or 'both'
    public function getVerificationMethod(): string

    protected function getGatewayName(): string
}
```

### 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.

```php theme={null}
'enabled' => (bool) $this->credentials()?->isConfigured(),
```

<Warning>
  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.
</Warning>

## Verification: pick one

| Value      | What happens                                         | Use when                               |
| ---------- | ---------------------------------------------------- | -------------------------------------- |
| `redirect` | Confirmed when the customer comes back               | No webhooks available                  |
| `webhook`  | Waits for the webhook, return page says "processing" | Provider is webhook-first              |
| `both`     | Redirect confirms right away, webhook reconciles     | **Default.** Good UX plus a safety net |

Four rules, and none of them are stylistic.

<AccordionGroup>
  <Accordion title="Never trust the redirect" icon="shield-halved">
    Re-query the provider inside `executePayment()`. Query strings are attacker-controlled.
  </Accordion>

  <Accordion title="Verify the webhook signature against the raw body, before parsing" icon="signature">
    ```php theme={null}
    $expected = hash_hmac('sha256', $request->getContent(), $secret);

    return hash_equals($expected, $signature);   // hash_equals, never ==
    ```
  </Accordion>

  <Accordion title="Assume webhooks arrive twice" icon="repeat">
    Providers retry. `handlePaymentSuccess()` has to be safe to call again for the same payment, so
    check the `PaymentToken` state first.
  </Accordion>

  <Accordion title="Check the amount and the currency" icon="scale-balanced">
    Not just the status. Confirm what was paid is what you asked for.
  </Accordion>
</AccordionGroup>

## Native subscriptions

<Note>Optional.</Note>

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.

| Class                            | Intercepts                                |
| -------------------------------- | ----------------------------------------- |
| `Services\CheckoutHandler`       | New service checkout                      |
| `Services\RenewalHandler`        | Renewals                                  |
| `Services\InvoicePaymentHandler` | Invoice payment (renewal and market only) |

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.

<Warning>
  The argument lists are long and positional: `handleCheckout()` takes 23, `handleRenewal()` 7,
  `handleInvoicePayment()` 6. `module:doctor` checks yours against those counts.
</Warning>

Declare it explicitly rather than relying on the class name:

```php theme={null}
public function getGatewayFlowHandler(): object
{
    return app(CheckoutHandler::class);
}
```

<Tip>
  `Modules/Gateways/BankWireGateway/Services/CheckoutHandler.php` is a short readable example: it
  creates the invoice and waits for proof of payment.
</Tip>

## Several checkout options from one module

<Note>Optional.</Note>

Implement `GatewayVariantInterface` to appear more than once, like "PayPal" and "PayPal Subscription".

```php theme={null}
public function getGatewayVariants(): ?array   // return null for normal behaviour
public function resolveVariant(string $variantKey): bool
public function getModuleName(): string
```

## Test it

```bash theme={null}
php artisan module:doctor AcmeGateway
```

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

<Steps>
  <Step title="New purchase" />

  <Step title="Renewal" />

  <Step title="Invoice payment" />

  <Step title="Upgrade" />

  <Step title="Balance top-up" />

  <Step title="Cancel halfway">Confirm no service is created.</Step>

  <Step title="Pay, then close the browser before the redirect">
    The webhook should still finish the order.
  </Step>

  <Step title="Replay the same webhook twice">
    Nothing should double-charge or double-provision.
  </Step>
</Steps>

<Tip>The last two are where real gateways break.</Tip>

## Worth reading

<CardGroup cols={2}>
  <Card title="BankWireGateway" icon="building-columns">
    Offline payment, no API at all.
  </Card>

  <Card title="CashfreeGateway" icon="lock">
    Clean HMAC webhook verification.
  </Card>

  <Card title="VippsGateway" icon="arrow-right-arrow-left">
    Straightforward redirect flow.
  </Card>

  <Card title="StripeGateway" icon="credit-card">
    Cards plus native subscriptions.
  </Card>

  <Card title="PayPalGateway" icon="layer-group">
    Everything, including variants and disputes.
  </Card>
</CardGroup>

<Info>
  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.
</Info>
