The biggest Savings of the year!

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

<?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 &raquo;%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>’ );

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:

<?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 );

}

}

}

 

3️⃣
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>’ ) );

}

}

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 ( ‘<gateway-name>’ !== payment_method ) {

return;

}

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

});

})();

5️⃣
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

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.