Ramadan Mubarak

00
Days
00
Hours
00
Minute
00
Second
> Home > docs > Developer Docs > StoreEngine Payment Gateway Plugin Development Guide

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).

1️⃣
Main Plugin File: storeengine-gateway-.php

This is the entry point of the plugin.

Key information:

  1. Plugin metadata (header)
  2. Dependency check & hook registration
  3. Gateway loading

Example:

* Plugin URI: https://wordpress.org/plugins//
* Description: Accept payments via .
* Author: Kodezen
* Author URI: 
* License: GPL-3.0+
* License URI: http://www.gnu.org/licenses/gpl-3.0.txt
* Text Domain: 
* 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
*/
/**
* 
*/
if ( ! defined( ‘ABSPATH’ ) ) {
exit;
}
define( ‘SE_GATEWAY__VERSION’, ‘1.0.0’ ); // WRCS: DEFINED_VERSION.
define( ‘SE_GATEWAY__MIN_PHP_VER’, ‘7.4’ );
define( ‘SE_GATEWAY__MIN_SE_VER’, ‘1.3’ );
define( ‘SE_GATEWAY__MAIN_FILE’, __FILE__ );
define( ‘SE_GATEWAY__ABSPATH’, __DIR__ . ‘/’ );
define( ‘SE_GATEWAY__PLUGIN_URL’, untrailingslashit( plugin_dir_url( SE_GATEWAY__MAIN_FILE ) ) );
define( ‘SE_GATEWAY__PLUGIN_PATH’, untrailingslashit( plugin_dir_path( SE_GATEWAY__MAIN_FILE ) ) );
/**
* StoreEngine fallback notice.
*/
function storeengine__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 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’, ‘’ ),
‘<strong>’,
‘</strong>’,
‘<a href="”http://wordpress.org/extend/plugins/storeengine/”">’,
‘</a>’,
‘<a>’,
‘</a>’
);
echo ‘’;
echo ‘<p>’ . wp_kses_post( $admin_notice_content ) . ‘</p>’;
echo ‘’;
}
function storeengine__storeengine_not_supported() {
/* translators: $1. Minimum StoreEngine version. $2. Current StoreEngine version. */
echo ‘<p><strong>’ . sprintf( esc_html__( ‘ requires StoreEngine %1$s or greater to be installed and active. StoreEngine %2$s is no longer supported.’, ‘’ ), esc_html( SE_GATEWAY__MIN_SE_VER ), esc_html( STOREENGINE_VERSION ) ) . ‘</strong></p>’;
}
function storeengine_gateway__check_dependencies() {
if ( ! class_exists( ‘StoreEngine’ ) ) {
add_action( ‘admin_notices’, ‘storeengine__missing_storeengine_notice’ );
return;
}
if ( version_compare( STOREENGINE_VERSION, SE_GATEWAY__MIN_SE_VER, ‘<‘ ) ) {
add_action( ‘admin_notices’, ‘storeengine__storeengine_not_supported’ );
}
}
add_action( ‘plugins_loaded’, ‘storeengine_gateway__check_dependencies’ );
function storeengine_gateway_() {
static $plugin;
 
// Load once.
if ( ! isset( $plugin ) ) {
require_once __DIR__ . ‘/includes/class-plugin.php’;
$plugin = \Storeengine\Plugin::get_instance();
}
return $plugin;
}
// Load plugin
add_action( ‘storeengine_loaded’, ‘storeengine_gateway_’ );
</strong>

2️⃣
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:

  1. Create singleton instance of this class
  2. Load gateway class and add necessary hook(s)

Example:

*/

namespace Storeengine;

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__MAIN_FILE ), [ $this, 'plugin_action_links' ] );

add_filter( 'storeengine/payment_gateways', [ $this, 'add_gateway' ] );

 

add_action( 'storeengine/gateway//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', '' ) . '</a>',

'docs' => '<a href="https://storeengine.pro/docs/how-to-set-up-payment-method-in-storeengine/#bkash-set-up" target="blank">' . esc_html__( 'Docs', '' ) . '</a>',

'support' => '<a href="https://storeengine.pro/support/" target="blank">' . esc_html__( 'Support', '' ) . '</a>',

] );

}

public function add_gateway( $gateways ) {

 // Include gateway file.

require_once SE_BKASH_PLUGIN_PATH . '/includes/class-gateway-.php';

$gateways[] = Gateway::class;

return $gateways;

}

 

public function load_assets( Gateway $gateway ) {

if ( $gateway->is_enabled() && $gateway->is_available() ) {

wp_enqueue_style( 'se-gateway-', SE_GATEWAY__PLUGIN_URL . '/assets/css/style.css', [], SE_GATEWAY__VERSION, 'all' );

wp_enqueue_script( 'se-gateway--vendor', 'https://js.example.com/v2/', [], false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion

wp_enqueue_script( 'se-gateway-', SE_GATEWAY__PLUGIN_URL . '/assets/js/script.js', ['se-gateway--vendor','jquery'], SE_GATEWAY__VERSION, true );

}

}

}

3️⃣
Gateway Class: includes/class-gateway-.php

Extends StoreEngine\Payment\Gateways\PaymentGateway class and defines payment flow logic for the gateway.

Gateway.

*

* @version 1.0.0

* @package Storeengine

*/

namespace Storeengine;

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 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__settings

$this->id = ‘’;

$this->icon = SE_GATEWAY__PLUGIN_URL . ‘/assets/images/icon.svg’;

// Title/description for admin interface.

$this->method_title = __( ‘’, ‘’ );

$this->method_description = __( ‘ works by adding payment fields on the checkout and then sending the details to  for verification.’, ‘’ );

// 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’, ‘’ ),

‘type’ => ‘safe_text’,

‘tooltip’ => __( ‘Payment method description that the customer will see on your checkout.’, ‘’ ),

‘default’ => __( ‘ Payment Gateway’, ‘’ ),

],

‘description’ => [

‘label’ => __( ‘Description’, ‘’ ),

‘type’ => ‘textarea’,

‘tooltip’ => __( ‘Payment method description that the customer will see on your website.’, ‘’ ),

‘default’ => __( ‘Pay with .’, ‘’ ),

],

‘is_production’ => [

‘label’ => __( ‘Is Live Mode?’, ‘’ ),

‘tooltip’ => __( ‘Enable  Live (Production) Mode.’, ‘’ ),

‘type’ => ‘checkbox’,

‘default’ => true,

],

‘api_key’ => [

‘label’ => __( ‘Application Key’, ‘’ ),

‘type’ => ‘text’,

‘tooltip’ => __( ‘Get your app Key from your  account.’, ‘’ ),

‘dependency’ => [ ‘is_production’ => true ],

‘autocomplete’ => ‘none’,

‘required’ => true,

],

‘api_secret’ => [

‘label’ => __( ‘Application Secret’, ‘’ ),

‘type’ => ‘password’,

‘tooltip’ => __( ‘Get your app Secret from your  account.’, ‘’ ),

‘dependency’ => [ ‘is_production’ => true ],

‘autocomplete’ => ‘none’,

‘required’ => true,

],

‘test_api_key’ => [

‘label’ => __( ‘Application Key (Sandbox)’, ‘’ ),

‘type’ => ‘text’,

‘tooltip’ => __( ‘Get your sandbox app Key from your  account.’, ‘’ ),

‘dependency’ => [ ‘is_production’ => false ],

‘autocomplete’ => ‘none’,

‘required’ => true,

],

‘test_api_secret’ => [

‘label’ => __( ‘Application Secret (Sandbox)’, ‘’ ),

‘type’ => ‘password’,

‘tooltip’ => __( ‘Get your sandbox app Secret from your  account.’, ‘’ ),

‘dependency’ => [ ‘is_production’ => false ],

‘autocomplete’ => ‘none’,

‘required’ => true,

],

‘a_checkbox’ => [

‘label’ => __( ‘A checkbox’, ‘’ ),

‘type’ => ‘checkbox’,

‘tooltip’ => __( ‘This is a checkbox.’, ‘’ ),

‘required’ => true,

‘default’ => true,

],

‘some_repeater’ => [

‘label’ => __( ‘Some repeater’, ‘’ ),

‘type’ => ‘repeater’,

‘fields’ => [

‘some_field’ => [

‘label’ => __( ‘Some-Field’, ‘’ ),

‘type’ => ‘string’,

‘tooltip’ => __( ‘Some-field\’s tooltip’, ‘’ ),

‘required’ => true,

],

‘other_field’ => [

‘label’ => __( ‘Other-Field’, ‘’ ),

‘type’ => ‘checkbox’,

‘tooltip’ => __( ‘Other-field\’s tooltip’, ‘’ ),

‘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( __( ‘ API key is required.’, ‘’ ), ‘api-key-is-required’, 400 );

}

if ( ! $api_secret ) {

throw new StoreEngineException( __( ‘ API secret key is required.’, ‘’ ), ‘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.’, ‘’ ),

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’, ‘’ ) ] );

}

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’, ‘’ ), $e->getMessage() )

);

throw $e;

} catch ( Exception $e ) {

$order->update_status(

OrderStatus::PAYMENT_FAILED,

/* translators: %s. Error details. */

sprintf( __( ‘Payment failed. Error: %s’, ‘’ ), $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)’, ‘’ ), $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.’, ‘’ ),

‘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’, ‘’ ), $e->getMessage() ) );

}

return $response;

}

/**

* Process refund via  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’, ‘’ ) );

}

}

4️⃣
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 ( '' !== payment_method ) {

return;

 }

 // Handle payment gateway popup or other api/ajax request...

 });

})();

5️⃣
Optional: uninstall.php

Clean up plugin data on uninstall.

Example:

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__REMOVE_ALL_DATA' ) || true !== SE_GATEWAY__REMOVE_ALL_DATA ) {

$settings = get_option( 'storeengine_payment__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__settings', $settings );

} else {

 // If SE_GATEWAY__REMOVE_ALL_DATA constant is set to true in the merchant's wp-config.php,

 // remove ALL plugin settings.

delete_option( 'storeengine_payment__settings' );

}

Screenshot: Settings

Payment Methods all setting

🧰
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.

  1. Use Order::set_paid_status( ‘status’ ) to update order payment status possible status are paid|unpaid|failed|on_hold, transaction_id.
  2. 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.