Here’s a comprehensive Developer Guide for creating a StoreEngine Payment Gateway as a standalone plugin.

Folder Structure
A standalone gateway plugin like storeengine-gateway- follows this structure:
storeengine-gateway-<gateway>/
├── storeengine-gateway-<gateway>.php # Main plugin file
├── uninstall.php # Cleanup logic
├── readme.txt # Plugin info
├── changelog.txt # Version history
├── license # License terms
├── .svnignore # Optional ignore rules
├── includes/
│ ├── class-gateway-<gateway>.php # Main gateway logic
│ └── class-plugin.php # Gateway implementation
└── assets/
└── css/
└── style.css
└── js/
└── script.js
└── images/
└── icon.svg
Replace with uppercase-safe ID (e.g., 2checkout, authorize.net).

Main Plugin File: storeengine-gateway-.php
This is the entry point of the plugin.
Key information:
- Plugin metadata (header)
- Dependency check & hook registration
- Gateway loading
Example:
<?php
/**
* Plugin Name: Storeengine Gateway <Gateway Name>
* Plugin URI: https://wordpress.org/plugins/<plugin-slug>/
* Description: Accept payments via <GatewayName>.
* Author: Kodezen
* Author URI: <AuthorURL>
* License: GPL-3.0+
* License URI: http://www.gnu.org/licenses/gpl-3.0.txt
* Text Domain: <textdomain>
* Domain Path: /languages
* Version: 1.0.0
* Requires PHP: 7.4
* Requires at least: 6.5
* Tested up to: 6.8
* SE tested up to: 1.3.1
*/
/**
* <Optional Copyright (c) declaration>
*/
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
define( ‘SE_GATEWAY_<GATEWAY>_VERSION’, ‘1.0.0’ ); // WRCS: DEFINED_VERSION.
define( ‘SE_GATEWAY_<GATEWAY>_MIN_PHP_VER’, ‘7.4’ );
define( ‘SE_GATEWAY_<GATEWAY>_MIN_SE_VER’, ‘1.3’ );
define( ‘SE_GATEWAY_<GATEWAY>_MAIN_FILE’, __FILE__ );
define( ‘SE_GATEWAY_<GATEWAY>_ABSPATH’, __DIR__ . ‘/’ );
define( ‘SE_GATEWAY_<GATEWAY>_PLUGIN_URL’, untrailingslashit( plugin_dir_url( SE_GATEWAY_<GATEWAY>_MAIN_FILE ) ) );
define( ‘SE_GATEWAY_<GATEWAY>_PLUGIN_PATH’, untrailingslashit( plugin_dir_path( SE_GATEWAY_<GATEWAY>_MAIN_FILE ) ) );
/**
* StoreEngine fallback notice.
*/
function storeengine_<gateway>_missing_storeengine_notice() {
$install_url = wp_nonce_url(
add_query_arg(
[
‘action’ => ‘install-plugin’,
‘plugin’ => ‘storeengine’,
],
admin_url( ‘update.php’ )
),
‘install-plugin_storeengine’
);
$admin_notice_content = sprintf(
// translators: 1$-2$: opening and closing <strong> tags, 3$-4$: link tags, takes to storeengine plugin on wp.org, 5$-6$: opening and closing link tags, leads to plugins.php in admin
esc_html__( ‘%1$sStoreEngine <gateway> gateway is inactive.%2$s The %3$sStoreEngine plugin%4$s must be active for the bKash Gateway to work. Please %5$sinstall & activate StoreEngine »%6$s’, ‘<textdomain>’ ),
‘<strong>’,
‘</strong>’,
‘<a href=”http://wordpress.org/extend/plugins/storeengine/”>’,
‘</a>’,
‘<a href=”‘ . esc_url( $install_url ) . ‘”>’,
‘</a>’
);
echo ‘<div class=”error”>’;
echo ‘<p>’ . wp_kses_post( $admin_notice_content ) . ‘</p>’;
echo ‘</div>’;
}
function storeengine_<gateway>_storeengine_not_supported() {
/* translators: $1. Minimum StoreEngine version. $2. Current StoreEngine version. */
echo ‘<div class=”error”><p><strong>’ . sprintf( esc_html__( ‘<Gateway> requires StoreEngine %1$s or greater to be installed and active. StoreEngine %2$s is no longer supported.’, ‘<textdomain>’ ), esc_html( SE_GATEWAY_<GATEWAY>_MIN_SE_VER ), esc_html( STOREENGINE_VERSION ) ) . ‘</strong></p></div>’;
}
function storeengine_gateway_<gateway>_check_dependencies() {
if ( ! class_exists( ‘StoreEngine’ ) ) {
add_action( ‘admin_notices’, ‘storeengine_<gateway>_missing_storeengine_notice’ );
return;
}
if ( version_compare( STOREENGINE_VERSION, SE_GATEWAY_<GATEWAY>_MIN_SE_VER, ‘<‘ ) ) {
add_action( ‘admin_notices’, ‘storeengine_<gateway>_storeengine_not_supported’ );
}
}
add_action( ‘plugins_loaded’, ‘storeengine_gateway_<gateway>_check_dependencies’ );
function storeengine_gateway_<gateway>() {
static $plugin;
// Load once.
if ( ! isset( $plugin ) ) {
require_once __DIR__ . ‘/includes/class-plugin.php’;
$plugin = \Storeengine<Gateway>\Plugin::get_instance();
}
return $plugin;
}
// Load plugin
add_action( ‘storeengine_loaded’, ‘storeengine_gateway_<gateway>’ );

Plugin Class: includes/class-plugin.php
Loads the gateway via StoreEngin’s gateway loading hook. This step is optional, but recommended for large plugins to keep the codes clean.
Key information:
- Create singleton instance of this class
- Load gateway class and add necessary hook(s)
Example:
<?php
/**
* Gateway initializer.
*
* @version 1.0.0
* @package Storeengine<Gateway>
*/
namespace Storeengine<Gateway>;
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
final class Plugin {
/**
* The *Singleton* instance of this class
*
* @var ?Plugin
*/
private static ?Plugin $instance = null;
/**
* Returns the *Singleton* instance of this class.
*
* @return Plugin The *Singleton* instance.
*/
public static function get_instance(): Plugin {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_filter( ‘plugin_action_links_’ . plugin_basename( SE_GATEWAY_<GATEWAY>_MAIN_FILE ), [ $this, ‘plugin_action_links’ ] );
add_filter( ‘storeengine/payment_gateways’, [ $this, ‘add_gateway’ ] );
add_action( ‘storeengine/gateway/<gateway-name>/init’, [ $this, ‘load_assets’ ] );
}
public function plugin_action_links( $links ) {
return array_merge( $links, [
‘settings’ => ‘<a href=”‘ . esc_url( admin_url( ‘admin.php?page=storeengine-settings&path=payment-method&gateway=<gateway>’ ) ) . ‘”>’ . esc_html__( ‘Settings’, ‘<textdomain>’ ) . ‘</a>’,
‘docs’ => ‘<a href=”https://storeengine.pro/docs/how-to-set-up-payment-method-in-storeengine/#bkash-set-up” target=”blank”>’ . esc_html__( ‘Docs’, ‘<textdomain>’ ) . ‘</a>’,
‘support’ => ‘<a href=”https://storeengine.pro/support/” target=”blank”>’ . esc_html__( ‘Support’, ‘<textdomain>’ ) . ‘</a>’,
] );
}
public function add_gateway( $gateways ) {
// Include gateway file.
require_once SE_BKASH_PLUGIN_PATH . ‘/includes/class-gateway-<gateway>.php’;
$gateways[] = Gateway<Gateway>::class;
return $gateways;
}
public function load_assets( Gateway<Gateway> $gateway ) {
if ( $gateway->is_enabled() && $gateway->is_available() ) {
wp_enqueue_style( ‘se-gateway-<gateway>’, SE_GATEWAY_<GATEWAY>_PLUGIN_URL . ‘/assets/css/style.css’, [], SE_GATEWAY_<GATEWAY>_VERSION, ‘all’ );
wp_enqueue_script( ‘se-gateway-<gateway>-vendor’, ‘https://js.example.com/v2/’, [], false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion
wp_enqueue_script( ‘se-gateway-<gateway>’, SE_GATEWAY_<GATEWAY>_PLUGIN_URL . ‘/assets/js/script.js’, [‘se-gateway-<gateway>-vendor’,‘jquery’], SE_GATEWAY_<GATEWAY>_VERSION, true );
}
}
}

Gateway Class: includes/class-gateway-.php
Extends StoreEngine\Payment\Gateways\PaymentGateway class and defines payment flow logic for the gateway.
<?php
/**
* The <Gateway> Gateway.
*
* @version 1.0.0
* @package Storeengine<Gateway>
*/
namespace Storeengine<Gateway>;
use Exception;
use StoreEngine\Classes\Exceptions\StoreEngineException;
use StoreEngine\Classes\Exceptions\StoreEngineInvalidOrderStatusException;
use StoreEngine\Classes\Exceptions\StoreEngineInvalidOrderStatusTransitionException;
use StoreEngine\Classes\Order;
use StoreEngine\Classes\OrderContext;
use StoreEngine\Classes\OrderStatus\OrderStatus;
use StoreEngine\Payment\Gateways\PaymentGateway;
use StoreEngine\Utils\Formatting;
use StoreEngine\Utils\Helper;
use WP_Error;
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
final class Gateway<Gateway> extends PaymentGateway {
public int $index = 1;
public function __construct() {
$this->setup();
$this->init_admin_fields();
$this->init_settings();
$this->title = $this->get_option( ‘title’ );
$this->description = $this->get_option( ‘description’ );
// While loading StoreEngine triggers separate gateway init hook, for each gateway with the
// reference of the gateway instance as only argument, so developer can hook into the process
// after initializing the gateway class and do things…
// add_action( ‘storeengine/gateway/’ . $this->id . ‘/init’, [ $this, ‘init’ ] );
// See the class-plugin.php example file.
}
protected function setup() {
// Id must be unique to this gateway.
// This will be used for saving the config data in options table as serialized array.
// Option key will be storeengine_payment_<gateway-name>_settings
$this->id = ‘<gateway-name>’;
$this->icon = SE_GATEWAY_<GATEWAY>_PLUGIN_URL . ‘/assets/images/icon.svg’;
// Title/description for admin interface.
$this->method_title = __( ‘<Gateway>’, ‘<textdomain>’ );
$this->method_description = __( ‘<Gateway> works by adding payment fields on the checkout and then sending the details to <Gateway> for verification.’, ‘<textdomain>’ );
// Checkout page field flag.
$this->has_fields = true;
// Verify config flag to run verify_config method while updating gateway settings from admin dashboard.
$this->verify_config = true;
// This can be [‘products’, ‘refund’,’subscriptions’,…] see the stripe addon.
$this->supports = [ ‘products’, ‘refunds’, ];
}
/**
* Loads field config for admin interface.
* This data will be used for rendering the gateway settings interface with react
* in admin-dashboard `StoreEngine ➔ Settings ➔ Payment Methods.`
*
* Field key will be used for storing the value as key=>value pair serialized array in options table
*
* Allowed field types: safe_text, text, textarea, password & checkbox. With special type `repeater` allowing all
* 5 types of filed as children
* Each field can have label (string), type (string), tooltip (string), default (bool|string|int|float), priority (int) data.
* Repeater should include the children in `fields` array and follow the same key=>field config.
* Default for repeater should be an array with the field keys.
*
* @return void
*/
protected function init_admin_fields() {
$this->admin_fields = [
‘title’ => [
‘label’ => __( ‘Title’, ‘<textdomain>’ ),
‘type’ => ‘safe_text’,
‘tooltip’ => __( ‘Payment method description that the customer will see on your checkout.’, ‘<textdomain>’ ),
‘default’ => __( ‘<Gateway> Payment Gateway’, ‘<textdomain>’ ),
],
‘description’ => [
‘label’ => __( ‘Description’, ‘<textdomain>’ ),
‘type’ => ‘textarea’,
‘tooltip’ => __( ‘Payment method description that the customer will see on your website.’, ‘<textdomain>’ ),
‘default’ => __( ‘Pay with <Gateway>.’, ‘<textdomain>’ ),
],
‘is_production’ => [
‘label’ => __( ‘Is Live Mode?’, ‘<textdomain>’ ),
‘tooltip’ => __( ‘Enable <Gateway> Live (Production) Mode.’, ‘<textdomain>’ ),
‘type’ => ‘checkbox’,
‘default’ => true,
],
‘api_key’ => [
‘label’ => __( ‘Application Key’, ‘<textdomain>’ ),
‘type’ => ‘text’,
‘tooltip’ => __( ‘Get your app Key from your <Gateway> account.’, ‘<textdomain>’ ),
‘dependency’ => [ ‘is_production’ => true ],
‘autocomplete’ => ‘none’,
‘required’ => true,
],
‘api_secret’ => [
‘label’ => __( ‘Application Secret’, ‘<textdomain>’ ),
‘type’ => ‘password’,
‘tooltip’ => __( ‘Get your app Secret from your <Gateway> account.’, ‘<textdomain>’ ),
‘dependency’ => [ ‘is_production’ => true ],
‘autocomplete’ => ‘none’,
‘required’ => true,
],
‘test_api_key’ => [
‘label’ => __( ‘Application Key (Sandbox)’, ‘<textdomain>’ ),
‘type’ => ‘text’,
‘tooltip’ => __( ‘Get your sandbox app Key from your <Gateway> account.’, ‘<textdomain>’ ),
‘dependency’ => [ ‘is_production’ => false ],
‘autocomplete’ => ‘none’,
‘required’ => true,
],
‘test_api_secret’ => [
‘label’ => __( ‘Application Secret (Sandbox)’, ‘<textdomain>’ ),
‘type’ => ‘password’,
‘tooltip’ => __( ‘Get your sandbox app Secret from your <Gateway> account.’, ‘<textdomain>’ ),
‘dependency’ => [ ‘is_production’ => false ],
‘autocomplete’ => ‘none’,
‘required’ => true,
],
‘a_checkbox’ => [
‘label’ => __( ‘A checkbox’, ‘<textdomain>’ ),
‘type’ => ‘checkbox’,
‘tooltip’ => __( ‘This is a checkbox.’, ‘<textdomain>’ ),
‘required’ => true,
‘default’ => true,
],
‘some_repeater’ => [
‘label’ => __( ‘Some repeater’, ‘<textdomain>’ ),
‘type’ => ‘repeater’,
‘fields’ => [
‘some_field’ => [
‘label’ => __( ‘Some-Field’, ‘<textdomain>’ ),
‘type’ => ‘string’,
‘tooltip’ => __( ‘Some-field\’s tooltip’, ‘<textdomain>’ ),
‘required’ => true,
],
‘other_field’ => [
‘label’ => __( ‘Other-Field’, ‘<textdomain>’ ),
‘type’ => ‘checkbox’,
‘tooltip’ => __( ‘Other-field\’s tooltip’, ‘<textdomain>’ ),
‘required’ => true,
]
],
‘default’ => [
[
‘some_field’ => ‘Some value’,
‘other_field’ => true,
],
],
],
];
}
/**
* Verify Config.
*
* @param array $config
*
* @return void
* @throws StoreEngineException
*/
public function verify_config( array $config ): void {
$is_production = $config[‘is_production’] ?? true;
if ( $is_production ) {
$api_key = $config[‘api_key’] ?? ”;
$api_secret = $config[‘api_secret’] ?? ”;
} else {
$api_key = $config[‘test_api_key’] ?? ”;
$api_secret = $config[‘test_api_secret’] ?? ”;
}
if ( ! $api_key ) {
throw new StoreEngineException( __( ‘<Gateway> API key is required.’, ‘<textdomain>’ ), ‘api-key-is-required’, 400 );
}
if ( ! $api_secret ) {
throw new StoreEngineException( __( ‘<Gateway> API secret key is required.’, ‘<textdomain>’ ), ‘api-secret-is-required’, 400 );
}
// Validate the credentials.
}
public function is_currency_supported( string $currency = null ): bool {
if ( ! $currency ) {
$currency = Formatting::get_currency();
}
return ‘BDT’ === $currency;
}
public function is_available(): bool {
if ( ! $this->is_currency_supported() ) {
return false;
}
return parent::is_available();
}
public function validate_minimum_order_amount( $order ) {
if ( $order->get_total() < 1 ) {
/* translators: 1) amount (including currency symbol) */
throw new StoreEngineException(
sprintf(
__( ‘Sorry, the minimum allowed order total is %1$s to use this payment method.’, ‘<textdomain>’ ),
Formatting::price( 1 )
),
‘did-not-meet-minimum-amount’
);
}
}
/**
* @param Order $order
*
* @return array|WP_Error
* @throws StoreEngineException
* @throws StoreEngineInvalidOrderStatusTransitionException
*/
public function process_payment( Order $order ) {
$payment_needed = $this->is_payment_needed( $order );
if ( $payment_needed ) {
$this->validate_minimum_order_amount( $order );
}
try {
$order_context = new OrderContext( $order->get_status() );
if ( $payment_needed ) {
$response = $this->call_gateway_api( $order, $order_context );
if ( is_wp_error( $response ) ) {
return $response;
}
} else {
$order->set_paid_status( ‘paid’ );
$order_context->proceed_to_next_status( ‘process_order’, $order, [ ‘note’ => _x( ‘Payment not needed.’, ‘bKash payment method’, ‘<textdomain>’ ) ] );
}
return [
‘result’ => ‘success’,
‘redirect’ => $order->get_checkout_order_received_url(),
];
} catch ( StoreEngineException $e ) {
$order->update_status(
OrderStatus::PAYMENT_FAILED,
/* translators: %s. Error details. */
sprintf( __( ‘Payment failed. Error: %s’, ‘<textdomain>’ ), $e->getMessage() )
);
throw $e;
} catch ( Exception $e ) {
$order->update_status(
OrderStatus::PAYMENT_FAILED,
/* translators: %s. Error details. */
sprintf( __( ‘Payment failed. Error: %s’, ‘<textdomain>’ ), $e->getMessage() )
);
throw new StoreEngineException( $e->getMessage(), ‘processing_error’, null, 0, $e );
}
}
protected function call_gateway_api( Order $order, OrderContext $order_context ) {
$response = (object) [
‘id’ => wp_generate_password(),
];
try {
$rand = random_int( 1, 11 );
if ( $rand % 3 === 0 ) {
sleep( 2 );
$order->set_paid_status( ‘paid’ );
$order_context->proceed_to_next_status( ‘process_order’, $order, [
/* translators: transaction id */
‘note’ => sprintf( __( ‘bKash charge complete (Charge ID: %s)’, ‘<textdomain>’ ), $response->id ),
‘transaction_id’ => $response->id,
] );
$order->save();
} else if ( $rand % 2 === 0 ) {
return new WP_Error( ‘unable-to-process’, ‘Unable to process payment. Please try after sometime.’ );
} else {
if ( random_int( 3, 7 ) % 3 === 0 ) {
$order->set_transaction_id( $response->id ); // Save the transaction ID to link the order to the Stripe charge ID. This is to fix reviews that result in refund.
$order->set_paid_status( ‘on_hold’ );
// Keep the order on hold for admin review.
$order_context->proceed_to_next_status( ‘hold_order’, $order, [
‘note’ => __( ‘Payment required review.’, ‘<textdomain>’ ),
‘transaction_id’ => $response->id,
] );
}
throw new Exception( ‘Some Error from.’ );
}
} catch ( Exception $e ) {
Helper::log_error( $e );
$order->set_paid_status( ‘failed’ );
/* translators: %s. Error message. */
$order_context->proceed_to_next_status( ‘payment_failed’, $order, sprintf( __( ‘Error while capturing the payment. Error: %s’, ‘<textdomain>’ ), $e->getMessage() ) );
}
return $response;
}
/**
* Process refund via <Gateway> API.
* @see Helper::create_refund()
* @see Helper::refund_payment()
* @see GatewayStripe::process_refund()
*
* @param int $order_id Order ID.
* @param float|string|null $amount Refund amount.
* @param string $reason Refund reason.
*
* @return bool|WP_Error True or false based on success, or a WP_Error object.
* @throws StoreEngineException
*/
public function process_refund( int $order_id, $amount = null, string $reason = ” ) {
return new WP_Error( ‘not-implemented’, __( ‘Feature not implemented’, ‘<textdomain>’ ) );
}
}

Script: assets/js/script.js
Handle submit with the custom event triggered by storeengine.
(function(){
document.addEventListener(‘storeengine-process-checkout’, function(event){
const {payment_method} = event.detail || {};
if ( ‘<gateway-name>’ !== payment_method ) {
return;
}
// Handle payment gateway popup or other api/ajax request…
});
})();

Optional: uninstall.php
Clean up plugin data on uninstall.
Example:
<?php
/**
* Storeengine Gateway <Gateway> Uninstall
*
* @version 1.0.0
*/
// Exit if accessed directly.
defined( ‘ABSPATH’ ) || exit;
// Exit if uninstall not called from WordPress.
defined( ‘WP_UNINSTALL_PLUGIN’ ) || exit;
// Remove cron jobs wp_clear_scheduled_hook()
/*
* ONLY remove the API keys and keep the other configuration.
* This is to prevent data loss when deleting the plugin from the backend
* and to ensure only the site owner can perform this action.
*/
if ( ! defined( ‘SE_GATEWAY_<GATEWAY>_REMOVE_ALL_DATA’ ) || true !== SE_GATEWAY_<GATEWAY>_REMOVE_ALL_DATA ) {
$settings = get_option( ‘storeengine_payment_<gateway-name>_settings’, [] );
if ( $settings && is_array( $settings ) ) {
// Remove API keys from the settings
unset( $settings[‘api_key’], $settings[‘api_secret’] );
unset( $settings[‘test_api_key’], $settings[‘test_api_secret’] );
}
update_option( ‘storeengine_payment_<gateway-name>_settings’, $settings );
} else {
// If SE_GATEWAY_<GATEWAY>_REMOVE_ALL_DATA constant is set to true in the merchant’s wp-config.php,
// remove ALL plugin settings.
delete_option( ‘storeengine_payment_<gateway-name>_settings’ );
}
Screenshot: Settings


Tools & Tips
StoreEngine keeps separate order status and payment status separate to make the process more flexible to integrate with different applications. To keep it simple StoreEngine provides multiple helper methods & classes to help with.
- Use Order::set_paid_status( ‘status’ ) to update order payment status possible status are paid|unpaid|failed|on_hold, transaction_id.
- Use OrderContext::proceed_to_next_status( ‘trigger’, $order, ‘note’ ) to update order status. Learn more about the trigger by checking the order status classes in storeengine/includes/classes/order-status/ directory.












