Use this file to discover all available pages before exploring further.
Gateway plugins add new payment methods to OwnPay — from global processors like Stripe and PayPal to regional options like bKash, GCash, or SEPA Direct Debit, and even cryptocurrency processors. When you build a gateway plugin, OwnPay’s GatewayBridge handles credential decryption, routes every payment flow to your adapter methods, and integrates your gateway into the standard checkout UI automatically.
When OwnPay boots, PluginLoader::loadActive() scans modules/gateways/, validates each manifest.json, token-scans every PHP file for dangerous functions, and instantiates the entrypoint class. Because a gateway class implements both PluginInterfaceandGatewayAdapterInterface, the loader automatically calls GatewayBridge::registerAdapter($instance) after the boot phase. From that point, every payment flow is routed through GatewayBridge:
Credentials are never passed raw. GatewayBridge calls GatewayConfigRepository::findCredentialsBySlug() for the active merchant, decrypts the stored JSON blob with FieldEncryptor, and passes the resulting plain-text key-value map to your adapter methods as the $credentials array.
A gateway plugin’s entrypoint must implement both OwnPay\Plugin\PluginInterface and OwnPay\Gateway\GatewayAdapterInterface. The GatewayDefaults trait provides safe, no-op fallbacks for every method — use it and override only what your gateway supports.
<?phpdeclare(strict_types=1);namespace OwnPay\Modules\Gateways\MyGateway;use OwnPay\Container;use OwnPay\Event\EventManager;use OwnPay\Gateway\GatewayAdapterInterface;use OwnPay\Gateway\GatewayDefaults;use OwnPay\Plugin\Capability;use OwnPay\Plugin\PluginInterface;final class MyGateway implements PluginInterface, GatewayAdapterInterface{ use GatewayDefaults; // ─── PluginInterface ──────────────────────────────────────────────────── public static function metadata(): array { return [ 'name' => 'My Gateway', 'slug' => 'my-gateway', 'version' => '1.0.0', 'description' => 'My custom payment gateway.', 'author' => 'Your Name', 'type' => 'gateway', ]; } public function capabilities(): array { return [Capability::GATEWAY]; } public function register(EventManager $events, Container $container): void { // Register event hooks here if needed. // GatewayBridge registration is handled automatically. } public function boot(Container $container): void {} public function deactivate(Container $container): void {} public function uninstall(Container $container): void { // Drop op_plugin_my_gateway_* tables here. } public function fields(): array { return [ [ 'name' => 'api_key', 'label' => 'API Key', 'type' => 'password', 'required' => true, ], [ 'name' => 'mode', 'label' => 'Mode', 'type' => 'select', 'options' => ['sandbox' => 'Sandbox', 'live' => 'Live'], 'required' => true, ], ]; } // ─── GatewayAdapterInterface ───────────────────────────────────────────── public function slug(): string { return 'my-gateway'; } public function initiate(array $params, array $credentials): array { // See: Processing Payments section below } public function verify(array $callbackData, array $credentials): array { // See: Handling Callbacks section below } public function verifyWebhook(string $rawBody, array $headers, array $credentials): bool { // See: Webhook Verification section below } public function refund(string $gatewayTrxId, string $amount, array $credentials): array { // See: Refund Logic section below } public function supports(string $feature): bool { return match ($feature) { 'refund', 'verification' => true, default => false, }; } public function supportedCurrencies(): array { return []; // Empty = accepts any currency }}
Never cast $params['amount'] to float. Floating-point arithmetic corrupts large values. Use the toMinorUnits() helper from GatewayDefaults for gateways that require integer cents, or toDecimalString() for those that accept decimal strings.
Your method must return one of three shapes:
public function initiate(array $params, array $credentials): array{ $apiKey = $credentials['api_key'] ?? ''; $mode = $credentials['mode'] ?? 'sandbox'; // Convert to minor units (cents) for APIs that require integers $amountCents = $this->toMinorUnits($params['amount']); // '150.00' → 15000 // Call the gateway API to create a payment session $response = $this->callGatewayApi('/sessions', $apiKey, [ 'amount' => $amountCents, 'currency' => strtolower($params['currency']), 'reference' => $params['trx_id'], 'return_url' => $params['redirect_url'], 'cancel_url' => $params['cancel_url'], ]); // Option 1 — Redirect to hosted payment page return ['redirect_url' => $response['checkout_url']]; // Option 2 — Render a self-hosted HTML form // return ['form_html' => '<form method="POST" action="https://gateway.example.com/pay">...</form>']; // Option 3 — Return a session ID for JS SDK-based checkout // return ['session_id' => $response['id']];}
verify() confirms a payment after the user returns from the gateway. The gateway’s own API is the only source of truth.
public function verify(array $callbackData, array $credentials): array{ $sessionId = (string) ($callbackData['session_id'] ?? ''); if ($sessionId === '') { return ['success' => false, 'gateway_trx_id' => '', 'status' => 'failed']; } // ALWAYS make a server-side API call — never trust $callbackData's status field $response = $this->callGatewayApi( '/sessions/' . urlencode($sessionId), $credentials['api_key'] ?? '' ); $paid = ($response['status'] ?? '') === 'paid'; return [ 'success' => $paid, 'gateway_trx_id' => (string) ($response['id'] ?? ''), 'amount' => $paid ? (string) ($response['amount_decimal'] ?? null) : null, 'status' => $paid ? 'completed' : 'failed', ];}
Security Rule: Never trust the payment status from $callbackData. A forged callback can manipulate the status field. Always make a server-to-server API call to verify the true payment status.
verifyWebhook() is called by GatewayBridge::verifyWebhookSignature() before the webhook payload is processed. The Stripe HMAC-SHA256 pattern — the reference implementation — demonstrates proper signature checking with replay protection:
public function verifyWebhook(string $rawBody, array $headers, array $credentials): bool{ $secret = $credentials['webhook_secret'] ?? ''; if ($secret === '') { // Fail closed — no secret means reject all webhooks return false; } $sigHeader = $headers['Stripe-Signature'] ?? $headers['stripe-signature'] ?? ''; if ($sigHeader === '') { return false; } // Parse "t=timestamp,v1=signature" $parts = []; foreach (explode(',', $sigHeader) as $item) { $kv = explode('=', $item, 2); if (count($kv) === 2) { $parts[trim($kv[0])] = trim($kv[1]); } } $timestamp = $parts['t'] ?? ''; $expectedSig = $parts['v1'] ?? ''; if ($timestamp === '' || $expectedSig === '') { return false; } // Replay protection — reject if older than 5 minutes if (abs(time() - (int) $timestamp) > 300) { return false; } $computed = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); // Always use hash_equals() — never === — for timing-safe comparison return hash_equals($computed, $expectedSig);}
The gateway.webhook.{slug} action hook fires after verifyWebhook() returns true. Your gateway can also listen to this hook inside register() to process the verified payload.
If your gateway does not support refunds, override supports('refund') to return false. OwnPay uses the supports() return value to determine whether to show the refund button in the transaction UI.
Use the mode field (declared in fields()) to switch between sandbox and live environments. The $credentials array contains the decrypted values your merchant configured.
public function initiate(array $params, array $credentials): array{ $apiKey = $credentials['api_key'] ?? ''; $mode = $credentials['mode'] ?? 'sandbox'; $baseUrl = $mode === 'live' ? 'https://api.mygateway.com/v1' : 'https://sandbox.mygateway.com/v1'; // Use $baseUrl for all API calls in this method}
Always provide a "sandbox" option in your fields()mode select. Operators need to test integrations without processing real payments.
Register hooks inside register() to react to payment events or modify checkout behavior:
public function register(EventManager $events, Container $container): void{ // Log every payment attempt through this gateway $events->addAction('gateway.capture.after', function (string $slug, array $result) use ($container): void { if ($slug !== 'my-gateway') { return; } // Log to op_plugin_my_gateway_logs... }, priority: 10); // Inject the gateway JS SDK into the checkout head $events->addAction('checkout.head', function (): void { echo '<script src="https://js.mygateway.com/v3"></script>'; });}
Hook
Type
Use Case
gateway.capture.before
Filter
Modify $params (amount, metadata) before initiate().