Skip to main content

Building a Payment Gateway

A StoreEngine payment gateway is a PHP class that extends StoreEngine\Payment\Gateways\PaymentGateway (includes/payment-gateways/payment-gateway.php), declares its id, settings, and capabilities, and implements process_payment(). The Payment_Gateways registry (includes/payment-gateways.php) discovers, boots, and materializes gateways lazily, so a store with 20 registered gateways only instantiates the two or three that are enabled.

This page shows the base class contract, how to register, and a minimal working gateway.

Use the modern base class

StoreEngine\Classes\AbstractPaymentGateway and AbstractPaymentGateways are deprecated. Always extend StoreEngine\Payment\Gateways\PaymentGateway.

The base class contract​

Extend PaymentGateway and set your identity and capabilities in the constructor. Key public/protected properties:

PropertyTypePurpose
$idstringUnique gateway id (e.g. stripe, razorpay). Drives its option key and hooks.
$title / $descriptionstringCustomer-facing title/description at checkout.
$method_title / $method_descriptionstringAdmin-facing name/description in settings.
$iconstringIcon URL.
$has_fieldsboolWhether the gateway renders extra fields at checkout.
$supportsarrayDeclared features (default ['products']).
$indexintSort order among gateways.
$max_amountintOptional max transaction amount (0 = no cap).
$saved_cardsboolWhether saving payment methods is allowed.
$admin_fieldsarrayThe settings-field schema.
$verify_configboolWhether saved settings need verification.

Methods you typically implement or override​

MethodWhen to override
__construct()Call setup(), init_admin_fields(), init_settings(), then read options.
process_payment( Order $order ): array|WP_ErrorRequired. Charge/confirm and return ['result' => 'success', 'redirect' => ...].
init_admin_fields()Populate $this->admin_fields with the settings schema.
is_available(): boolAdd gateway-specific availability rules (currency, page context).
supports( string $feature ): boolUsually just declare $supports; override for dynamic logic.
process_refund( int $order_id, $amount = null, string $reason = '' )Implement if you declare refunds support.
payment_fields()Render custom checkout fields when $has_fields is true.
validate_fields(): voidThrow a StoreEngineException to block an invalid submission.
verify_config( array $config )Validate saved credentials when $verify_config is true.
process_scheduled_payment( Order $renewal_order ): voidCharge subscription renewals / installments (see below).

The $supports catalog​

Declaring a feature turns on the related behavior. Common flags seen across core gateways: products, refunds, subscriptions, multiple_subscriptions, subscription_cancellation, subscription_reactivation, subscription_suspension, subscription_amount_changes, subscription_date_changes, subscription_payment_method_change (and _admin / _customer variants), tokenization, add_payment_method, and gateway_scheduled_payments.

If you support gateway_scheduled_payments, call $this->register_subscription_hooks() at the end of your constructor and implement process_scheduled_payment() — it wires both subscription renewals and installment-plan payments to that single method.

Settings storage​

PaymentGateway persists settings to the option storeengine_payment_{$id}_settings. Use get_option( $key ) / update_option( $key, $value ) on the instance (not the WordPress functions of the same name) — they read/write that option and merge in the defaults declared in $admin_fields. Every field also gets an implicit is_enabled (bool) and index (int).

How gateways register​

Two mechanisms, both filters. Prefer the lazy registry for anything that shouldn't be built until needed.

Eager (legacy) — bare class names​

The core offline gateways (COD, BACS, Check) and the in-core Stripe/PayPal/Razorpay addons register a class name on storeengine/payment_gateways. The registry instantiates these on init (the set is small and bounded, and it can't learn the id without constructing them):

add_filter( 'storeengine/payment_gateways', function ( array $gateways ) {
$gateways[] = \My\Plugin\GatewayFoo::class;
return $gateways;
} );

Lazy — id => class-string or factory​

Register id => class-string|callable on storeengine/payment_gateway_classes. Gateways registered here are not instantiated until an enabled gateway appears on checkout, the admin settings screen loads, or a by-id lookup (refund, renewal, webhook) needs it. A callable factory lets you return a pre-configured shared base without a subclass per provider:

add_filter( 'storeengine/payment_gateway_classes', function ( array $gateways ) {
$gateways['foo'] = \My\Plugin\GatewayFoo::class;
$gateways['mollie'] = fn() => new \My\Plugin\AbstractRedirectGateway( $mollie_config );
return $gateways;
} );

Per-gateway init hook​

When an enabled gateway is booted, the registry fires storeengine/gateway/{id}/init with the instance by reference — the place to register frontend assets or checkout glue:

add_action( 'storeengine/gateway/foo/init', function ( $gateway ) {
// Register scripts, service singletons, etc.
} );

The registry also fires storeengine/payment_gateways_initialized (passing the Payment_Gateways instance) once the registry is built — note this fires after registration, not after every gateway is instantiated.

Availability​

is_available() returns true when the gateway is enabled, within $max_amount, and compatible with the cart/order. The base implementation already excludes gateways that don't supports('subscriptions') from carts/orders that contain a subscription, and enforces $max_amount. Override to add currency or context checks, then call parent::is_available():

public function is_available(): bool {
if ( ! $this->is_currency_supported() ) {
return false;
}
return parent::is_available();
}

Processing a payment​

process_payment() runs during checkout / order-pay. It should move the order to its next status and return a result array with a redirect (usually the order-received URL). Return a WP_Error or a ['result' => 'failure'] array on failure.

public function process_payment( Order $order ): array {
// ...charge via your SDK...
$order->set_paid_status( 'paid' );
$context = new \StoreEngine\Classes\OrderContext( $order->get_status() );
$context->proceed_to_next_status( 'process_order', $order );
$order->save();

return [
'result' => 'success',
'redirect' => $order->get_checkout_order_received_url(),
];
}

Client-side intents (optional)​

Gateways that need a pre-confirmation client step (Stripe PaymentIntents, PayPal/Razorpay order creation) implement StoreEngine\Interfaces\GatewayAdapterInterface. Its single method, create_intent( Order $order, Cart $cart ), returns a normalized payload (any of client_secret, intent_id, redirect_url, requires_action) shared by both the jQuery checkout and the React checkout via POST /storeengine/v1/checkout/payment-intent/{gateway_id}. Offline/COD gateways don't need this — place_order runs process_payment() directly.

Minimal skeleton​

<?php
namespace My\Plugin;

use StoreEngine\Payment\Gateways\PaymentGateway;
use StoreEngine\Classes\Order;
use StoreEngine\Classes\OrderContext;

class GatewayFoo extends PaymentGateway {

protected int $index = 5;

public function __construct() {
$this->setup();
$this->init_admin_fields();
$this->init_settings();

$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
}

protected function setup() {
$this->id = 'foo';
$this->method_title = __( 'Foo Pay', 'my-plugin' );
$this->method_description = __( 'Accept payments via Foo.', 'my-plugin' );
$this->has_fields = false;
$this->verify_config = true;
$this->supports = [ 'products', 'refunds' ];
}

protected function init_admin_fields() {
$this->admin_fields = [
'title' => [
'label' => __( 'Title', 'my-plugin' ),
'type' => 'safe_text',
'default' => __( 'Foo Pay', 'my-plugin' ),
'priority' => 0,
],
'api_key' => [
'label' => __( 'API Key', 'my-plugin' ),
'type' => 'password',
'required' => true,
'priority' => 1,
],
];
}

public function verify_config( array $config ) {
if ( empty( $config['api_key'] ) ) {
throw new \StoreEngine\Classes\Exceptions\StoreEngineException(
esc_html__( 'API Key is required.', 'my-plugin' ),
'api-key-required', 400
);
}
}

public function process_payment( Order $order ): array {
// ...call the Foo SDK with $order->get_total() / $order->get_currency()...
$order->set_paid_status( 'paid' );
( new OrderContext( $order->get_status() ) )
->proceed_to_next_status( 'process_order', $order );
$order->save();

return [
'result' => 'success',
'redirect' => $order->get_checkout_order_received_url(),
];
}

public function process_refund( int $order_id, $amount = null, string $reason = '' ) {
// ...call the Foo refund API; return true on success, WP_Error on failure...
return true;
}
}

Register it lazily:

add_filter( 'storeengine/payment_gateway_classes', function ( array $gateways ) {
$gateways['foo'] = \My\Plugin\GatewayFoo::class;
return $gateways;
} );

Settings-field types​

The type of an $admin_fields entry maps to the admin form control and validation. Common types seen in core gateways: text, safe_text, password, textarea, checkbox, multiselect, and copy_text (a read-only display field). password and copy_text validate as text; checkbox validates as boolean.

See also​