This guide covers everything required to build, install, and maintain a payment gateway plugin for OwnPay. Every example is derived from the live Stripe gateway implementation at modules/gateways/stripe/.
How gateway plugins work
When OwnPay boots, PluginLoader::loadActive() scans modules/gateways/, discovers all active gateway directories, validates each manifest.json, token-scans every PHP file for dangerous functions, and instantiates the entrypoint class.
Because a gateway class implements both PluginInterface and GatewayAdapterInterface, the loader automatically calls GatewayBridge::registerAdapter($instance) after booting all plugins.
Credentials are never passed raw. GatewayBridge calls GatewayConfigRepository::findCredentialsBySlug(), decrypts the stored JSON blob, and passes the plain-text key-value map to your adapter methods.
Directory structure
Place your gateway inside modules/gateways/{your-slug}/:
The manifest.json file
The entrypoint class
A gateway plugin’s entrypoint class must implement both OwnPay\Plugin\PluginInterface and OwnPay\Gateway\GatewayAdapterInterface:
Method contracts
initiate(array $params, array $credentials): array
Called to start a payment session. $params has this guaranteed shape:
Return one of:
['redirect_url' => 'https://gateway.example.com/pay/SESSION_ID']
['form_html' => '<form>...</form>']
['session_id' => 'SESSION_ID']
verify(array $callbackData, array $credentials): array
Security Rule: Never trust the payment status from $callbackData. Always make a server-to-server API call.
verifyWebhook(string $rawBody, array $headers, array $credentials): bool
Use hash_equals() for all HMAC comparisons. Include replay protection by rejecting stale timestamps.
refund(string $gatewayTrxId, string $amount, array $credentials): array
$gatewayTrxId is the value your verify() returned. $amount is a BCMath-safe decimal string.
supports(string $feature): bool
Multi-file plugins
Declare a namespace in manifest.json. The PluginLoader registers a PSR-4 autoloader mapping {namespace}\{SubClass} to {plugin_dir}/{SubClass}.php.
Database migrations
Create a migrations/ directory with .sql files. Plugin tables must be prefixed with op_plugin_.
Content Security Policy
If your gateway loads external JavaScript, declare the required domains in csp inside manifest.json.
Security requirements
- Never trust callback data - always verify server-side
- Use
hash_equals() for all HMAC comparisons
- Reject webhook timestamps older than 5 minutes
- If
webhook_secret is missing, verifyWebhook() must return false
- No
eval() or OS commands
- No direct
op_* table access
Installation and activation
- Package your plugin directory as a ZIP
- Upload via Admin > Gateways > “Install Plugin”
- Activate via the gateway list
- Configure credentials via the gateway settings form
Checklist
manifest.json present with all required fields
- Entrypoint implements both
PluginInterface and GatewayAdapterInterface
slug() returns the exact value in manifest.json
initiate() returns a valid response shape
verify() makes a server-side API call
verifyWebhook() uses hash_equals() and enforces timestamp replay window
- Plugin tables use the
op_plugin_ prefix
- No use of
eval(), exec(), shell_exec()
- CSP domains declared in
manifest.json
Related pages
Last modified on August 25, 2026