Payment Flow & Testing
Payment Flow
1. Gateway Registration (boot time)
Plugin registers via $this->ctx->registry->register('gateway', ...) with:
id— Gateway identifier used in checkout formslabel— Display nameipn_route— Named route for webhooksettings_prefix— Prefix forstatic_optionskeysenabled_option— Option key that enables/disables gateway
2. Checkout Selection (user-facing)
Core's PaymentGatewayList builds the gateway list from built-in + plugin-declared gateways. Each gateway appears only if get_static_option('{prefix}_gateway') is non-empty.
3. Payment Initiation (core side)
When user selects a plugin gateway:
- Core validates gateway is in allowed list
- Creates pending record (WalletHistory/Order/Subscription)
- Builds
payment_argsarray with amount, track ID, IPN URL, etc. - Fires
payment.chargefilter viaplugin_charge_payment() - Plugin returns a redirect
Responseornull
4. Charge Handling (plugin side)
Plugin hooks into payment.charge:
- Checks if the gateway matches its ID
- Builds provider-specific payment URL
- Returns redirect Response to hosted payment page
5. IPN/Callback (provider calls back)
Provider POSTs to your IPN endpoint. Plugin:
- Verifies authenticity (HMAC signature, API key)
- Parses provider data
- Calls
plugin_apply_filters('payment.complete', ...)to delegate to core
6. Core Completion
PaymentCompletionService::complete() handles:
| Payment Type | Model | Action |
|---|---|---|
client-wallet | WalletHistory | Credit wallet, fire wallet.deposit.completed |
order | Order | Mark paid, fire order.paid |
subscription | UserSubscription | Activate, fire subscription.activated |
promotion | Plugin model | Delegates to payment.complete.promotion filter |
Core Bridge Functions
| Function | Purpose |
|---|---|
plugin_gateways() | Get all registered gateways |
plugin_gateway_declared($id) | Check if gateway is registered |
plugin_gateway_ipn_route($id) | Get IPN route name |
plugin_charge_payment($gateway, $args) | Fire payment.charge filter |
plugin_apply_filters($hook, $value, ...) | Fire any filter |
plugin_do_action($hook, ...$args) | Fire any action |
get_static_option($key, $default) | Read setting |
update_static_option($key, $value) | Write setting |
Architectural Invariants
- Core never references plugins by name — all wiring via registry and filters
- IPN endpoints are public — no CSRF, no session (use
routes/api.php) - Core owns completion — plugins verify authenticity, core handles business logic
- Completion is idempotent — duplicate webhooks are safe (checks
payment_status != 'complete') - Gateway presence depends solely on
{prefix}_gatewaystatic option - No per-gateway core edits —
payment.chargefallback handles all plugin gateways - Settings live in
static_options— no new tables required for gateway config payment_typecontext tells core what to complete:client-wallet,order,subscription,promotion
Testing
Manual Testing
- Enable gateway in admin settings
- Set test mode and test API keys
- Create a wallet deposit or order
- Select gateway at checkout
- Complete payment on provider's sandbox
- Verify IPN received and wallet/order updated
Webhook Testing
Use tools like ngrok to expose local IPN endpoint:
ngrok http 80
Configure provider to send webhooks to the ngrok URL.
Reference: Existing Gateway Plugins
| Plugin | Location | Pattern |
|---|---|---|
| SePay | plugins/sepay-payment-gateway/ | Custom checkout page, QR code, polling |
| CoinPayments | plugins/coin-payment-gateway/ | Uses XgPaymentGateway library |
| YooMoney | plugins/yoomoney-payment-gateway/ | Uses XgPaymentGateway library |
Last updated: September 2026
Still stuck?
Our support team is ready to help you get set up.

