Skip to content

Payment Gateways — Developer Guide

Contracts

php
use App\Billing\Contracts\PaymentGatewayInterface;
use App\Billing\GatewayManager;

$driver = app(GatewayManager::class)->driver('stripe'); // or 'creem' / default
$driver->createCheckout($request);
$driver->parseWebhook($payload, $signature);
$driver->testConnection();
$driver->capabilities();
$driver->supportedCurrencies();

Never import Stripe\*, Laravel\Cashier\*, StripeGateway, or CreemGateway from BillingEngine, services outside App\Billing\Drivers, or controllers other than Stripe-specific webhook adapters.

Creem customer portal: CreemGateway::createPortalSession(Tenant $tenant) (driver helper; not required by BillingEngine).

Encrypted credentials

payment_gateways.config uses Eloquent encrypted:array. API resources return redacted config:

  • Non-secret keys (e.g. publishable_key) returned as plain values
  • Secret keys (secret_key, webhook_secret, *_secret) returned as { "configured": true|false }

Config updates merge; empty secret fields mean “leave unchanged”. Changes are written to gateway_logs and Spatie activity log (payment_gateway.config_updated) with changed key names only.

Admin API (Central v1)

MethodPathPurpose
GET/payment-gatewaysList
GET/payment-gateways/{id}Detail (redacted config)
POST…/enable / …/disableToggle
POST…/defaultPlatform default
PUT…/configCredentials
PUT…/modesandbox | live
POST…/test-connectionDriver connectivity probe
GET…/webhook-statusHealth + endpoint hint
GET…/logsGateway operational logs
GET…/webhook-logsInbound webhook audit
GET…/capabilitiesCapabilities + currencies

Permissions: payment-gateways.list|read|update (or billing.manage).

Webhooks

  • Generic: POST /webhooks/gateways/{code}GatewayWebhookController
  • Stripe + Cashier mirror: POST /stripe/webhookStripeWebhookController

Both normalize via PaymentGatewayInterface::parseWebhook() and call BillingEngine::handleGatewayEvent(). Drivers should set GatewayEvent::$tenantId when they can resolve the workspace, so the engine never queries provider-specific columns. When tenantId is missing, BillingEngine::resolveTenant also tries providerSubscriptionId and meta.payment_id (needed for payment_failed without a customer map).

CSRF exceptions: stripe/*, webhooks/gateways/*.

Schema notes

TablePurpose
payment_gatewaysRegistry + encrypted config + mode + webhook health
payment_methodsWorkspace saved methods / preferred method
payments / payment_transactionsLedger
payment_attemptsCheckout/charge attempts
gateway_logsAdmin/driver operational events
webhook_logsInbound webhook audit
tenant_gateway_customersProvider-neutral customer_reference per gateway (Creem+)

No creem_* / provider-specific customer columns. Stripe still uses Cashier’s tenants.stripe_id.

Tests

bash
php artisan test --compact tests/Feature/Central/Billing/PaymentGatewayManagementTest.php
php artisan test --compact tests/Feature/Central/Billing/PaymentGatewayIsolationTest.php
php artisan test --compact tests/Feature/Central/Billing/GatewayWebhookRoutingTest.php
php artisan test --compact tests/Feature/Central/Billing/CreemGatewayTest.php
php artisan test --compact tests/Feature/Central/Billing/CreemWebhookTest.php

Architecture tests assert BillingEngine and GatewayManager do not reference Stripe/Cashier/Creem drivers.

Official documentation for the SaleOS SaaS Platform.