
August 13, 2026
12 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Most Nepal-based eCommerce stores eventually hit a wall where standard Stripe or PayPal plugins cannot handle local banking realities like eSewa, Khalti, ConnectIPS, or Fonepay. WooCommerce custom payment gateway development solves this by letting you build a dedicated plugin that speaks directly to domestic APIs while maintaining full checkout compatibility. If you are an eCommerce website developer in Nepal or a founder managing cross-border sales, understanding this architecture is the difference between abandoned carts and completed transactions.
WC_Payment_Gateway class to create a plugin that processes payments via external APIs like eSewa or Khalti. It requires implementing process_payment(), secure webhook verification, and proper order status handling within WordPress 6.7+ and WooCommerce 9.x environments.How do you structure a WooCommerce custom payment gateway plugin?
A common mistake in WooCommerce custom payment gateway development is dumping all logic into a single file or modifying core WooCommerce files directly. In production, you must treat the gateway as a standalone plugin that hooks into WooCommerce's lifecycle without touching vendor code. This ensures your integration survives WooCommerce 9.x updates and remains portable across client sites.
The main plugin file should only contain the plugin header, a WooCommerce active check, and an include statement for your gateway class. All business logic belongs in includes/class-wc-gateway-custom.php. This separation matters because when you hand off a project to another developer or need to debug a live issue at 2 AM, knowing exactly where the API signature verification lives saves hours.
<?php
/**
* Plugin Name: My Custom Gateway
* Description: Integrates Nepal payment providers with WooCommerce
* Version: 1.0.0
* Requires at least: 6.7
* WC requires at least: 9.0
*/
if ( ! defined( 'ABSPATH' ) ) exit;
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'WooCommerce' ) ) return;
require_once plugin_dir_path( __FILE__ ) . 'includes/class-wc-gateway-custom.php';
add_filter( 'woocommerce_payment_gateways', function( $gateways ) {
$gateways[] = 'WC_Gateway_Custom_Nepal';
return $gateways;
});
}); This bootstrap pattern prevents fatal errors if WooCommerce is deactivated and ensures your gateway registers only when the environment supports it. On real client projects, I have seen sites break completely after a WooCommerce update because the plugin lacked these guards. Always validate dependencies before loading classes.
How do you implement the WC_Payment_Gateway class correctly?
The heart of WooCommerce custom payment gateway development is the gateway class itself. You must extend WC_Payment_Gateway and implement specific methods that WooCommerce calls during checkout. Getting the constructor right sets up admin settings, icons, and supported features that determine whether your gateway appears at all on the frontend.
class WC_Gateway_Custom_Nepal extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'custom_nepal_gateway';
$this->icon = apply_filters( 'wc_custom_nepal_icon', plugins_url( '/assets/icon.png', __FILE__ ) );
$this->method_title = __( 'Custom Nepal Gateway', 'wc-custom-nepal' );
$this->method_description = __( 'Accept payments via eSewa, Khalti, or bank transfer.', 'wc-custom-nepal' );
$this->has_fields = true;
$this->supports = array( 'products', 'refunds' );
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
}
} The $this->supports array is critical. If you omit 'refunds', WooCommerce will not show the refund button on orders paid through your gateway, forcing manual database edits later. Similarly, setting $this->has_fields = true tells WooCommerce to call your payment_fields() method, which is where you render custom form inputs like QR codes or bank account selectors.
Defining admin configuration fields
Your gateway needs configurable API credentials, test mode toggles, and webhook secrets. Define these in init_form_fields() using WooCommerce's settings API format. Never hardcode secrets or use $_POST directly in your processing logic.
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'wc-custom-nepal' ),
'type' => 'checkbox',
'label' => __( 'Enable Custom Nepal Gateway', 'wc-custom-nepal' ),
'default' => 'no',
),
'api_key' => array(
'title' => __( 'API Key', 'wc-custom-nepal' ),
'type' => 'password',
'description' => __( 'Your merchant API key from the provider dashboard.', 'wc-custom-nepal' ),
'default' => '',
),
'test_mode' => array(
'title' => __( 'Test Mode', 'wc-custom-nepal' ),
'type' => 'checkbox',
'label' => __( 'Enable sandbox testing', 'wc-custom-nepal' ),
'default' => 'yes',
'description' => __( 'Uses test endpoints and dummy credentials.', 'wc-custom-nepal' ),
),
);
} Using 'password' type for API keys masks them in the admin UI and prevents accidental exposure in screenshots or screen shares. On legal-tech portals I have built, clients often share admin access with multiple staff members; masking sensitive fields is a basic security hygiene practice that costs nothing but prevents costly credential leaks.
How does the payment processing and webhook flow work?
Understanding the asynchronous nature of modern payment APIs is where most WooCommerce custom payment gateway development projects fail. Unlike credit card gateways that return immediate success/failure, Nepal payment providers typically redirect users to their app, then notify your server via webhook minutes or hours later. Your code must handle both the initial redirect and the delayed confirmation without creating duplicate orders or leaving payments in limbo.
The process_payment() method handles step 2 in the diagram above. It must create a pending order, store the transaction reference, and return a redirect URL. Crucially, it should never mark the order as complete here — that happens only when the webhook arrives.
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
// Generate unique transaction reference
$transaction_ref = 'TXN-' . $order_id . '-' . time();
$order->update_meta_data( '_custom_gateway_txn_ref', $transaction_ref );
$order->set_transaction_id( $transaction_ref );
$order->save();
// Build payment URL with callback parameters
$payment_url = add_query_arg( array(
'merchant_id' => $this->get_option( 'api_key' ),
'amount' => $order->get_total(),
'transaction_id' => $transaction_ref,
'success_url' => $this->get_return_url( $order ),
'failure_url' => wc_get_checkout_url(),
), $this->get_api_endpoint() );
return array(
'result' => 'success',
'redirect' => $payment_url,
);
} Handling webhooks securely
Webhook endpoints are public URLs that anyone can POST to. Without signature verification, attackers could mark arbitrary orders as paid. Register your webhook handler early in WordPress initialization, verify the signature before touching any order data, and always return HTTP 200 quickly to prevent provider retries.
add_action( 'rest_api_init', function() {
register_rest_route( 'wc-custom-nepal/v1', '/webhook', array(
'methods' => 'POST',
'callback' => array( 'WC_Gateway_Custom_Nepal', 'handle_webhook' ),
'permission_callback' => '__return_true', // Public endpoint, auth via signature
));
});
public static function handle_webhook( WP_REST_Request $request ) {
$payload = $request->get_body();
$signature = $request->get_header( 'X-Signature' );
$secret = get_option( 'wc_custom_nepal_webhook_secret' );
// Verify HMAC-SHA256 signature
$expected = hash_hmac( 'sha256', $payload, $secret );
if ( ! hash_equals( $expected, $signature ) ) {
wc_get_logger()->error( 'Invalid webhook signature', array( 'source' => 'custom-nepal-gateway' ) );
return new WP_REST_Response( 'Invalid signature', 403 );
}
$data = json_decode( $payload, true );
$order = wc_get_order( $data['transaction_id'] );
if ( $order && $order->has_status( 'pending' ) ) {
$order->payment_complete( $data['provider_reference'] );
$order->add_order_note( 'Payment confirmed via webhook. Ref: ' . $data['provider_reference'] );
}
return new WP_REST_Response( 'OK', 200 );
} Using hash_equals() instead of === prevents timing attacks that could leak signature information. This is a detail many tutorials skip, but on financial systems handling NPR transactions worth lakhs, cryptographic correctness is non-negotiable. For more context on securing API integrations, see the guide on Laravel Khalti and eSewa Nepal payment integration which covers similar verification patterns outside WooCommerce.
What are the differences between redirect and embedded gateway approaches?
Choosing between redirect and embedded payment flows fundamentally shapes your WooCommerce custom payment gateway development effort, user experience, and PCI compliance scope. Neither approach is universally better; the right choice depends on your provider's API capabilities, your team's frontend skills, and how much checkout friction your customers tolerate.
| Criteria | Redirect Gateway | Embedded / iframe Gateway |
|---|---|---|
| User Experience | Leaves site, returns via callback | Stays on checkout page throughout |
| Implementation Complexity | Low — simple URL redirect | High — JS SDK, CORS, tokenization |
| PCI Scope | Minimal — provider handles card data | Expanded — your domain touches payment UI |
| Mobile Conversion | Lower — app switching causes drop-off | Higher — seamless in-app experience |
| Nepal Provider Support | eSewa, Khalti, ConnectIPS (standard) | Limited — mostly international gateways |
| Debugging Difficulty | Easier — clear redirect boundaries | Harder — JS errors, iframe restrictions |
| Best For | Local banks, UPI, wallet payments | Credit cards, subscription billing |
In my experience building eCommerce platforms for Nepal florists and grocery stores, redirect gateways win for domestic payments because eSewa and Khalti are designed as app-first experiences. Users expect to open their banking app, authenticate via biometrics, and return to the merchant site. Trying to embed these flows in an iframe fights against both provider terms and user mental models. Reserve embedded approaches for international card processing where staying on-site genuinely improves conversion.
How do you test and debug custom payment gateways safely?
Testing WooCommerce custom payment gateway development in production is reckless, yet many developers skip staging environments because setting up webhook tunnels feels tedious. Invest thirty minutes configuring ngrok or Cloudflare Tunnel once, and you will save days of debugging silent webhook failures later. Most Nepal payment providers offer sandbox environments with test credentials that mirror production behavior without moving real money.
- Enable test mode first: Set
test_mode = yesin gateway settings and use sandbox API keys. Verify the plugin switches endpoints correctly by checking logs. - Create test products: Use low-value items (Rs 10–50) to avoid accidental real charges. Configure shipping zones to match your target market.
- Tunnel webhooks locally: Run
ngrok http 8080and register the HTTPS tunnel URL in your provider's dashboard. Update it each time ngrok restarts unless you pay for fixed subdomains. - Log everything: Add
wc_get_logger()->info()calls at every decision point. Include request headers, payload hashes, and order IDs. Production debugging without logs is guesswork. - Simulate failure states: Test expired tokens, insufficient funds, network timeouts, and invalid signatures. Happy-path testing alone misses 80% of production bugs.
- Verify order notes: Confirm that successful payments add descriptive notes with provider references. Customers and support staff rely on these for dispute resolution.
One gotcha I encounter repeatedly: ngrok free tier assigns random subdomains on restart. If you forget to update the webhook URL in your provider dashboard after restarting the tunnel, webhooks silently fail and orders stay pending forever. Either bookmark the dashboard page or invest Rs 1,000/month (~USD 7.50) in a fixed subdomain. The time saved during debugging pays for itself within a week.
When should you hire a specialist versus building in-house?
Not every team should attempt WooCommerce custom payment gateway development internally. The decision hinges on your timeline, regulatory requirements, and long-term maintenance capacity. Building in-house gives you full control but demands ongoing attention as providers change APIs, WordPress updates shift hooks, and security standards evolve.
Hire a specialist when your gateway handles high transaction volumes, integrates with regulated financial institutions, or requires complex reconciliation logic. A developer who has shipped multiple Nepal payment integrations already knows which eSewa endpoints timeout under load, how Khalti structures refund responses, and why ConnectIPS webhooks sometimes arrive out of order. That institutional knowledge compresses months of trial-and-error into weeks of predictable delivery. If you are evaluating this path, reviewing how to hire a reliable Nepali website developer helps set realistic expectations for scope and pricing.
Build in-house when the integration is straightforward (single provider, standard redirect flow), your team has prior WooCommerce plugin experience, and you have budget allocated for ongoing maintenance. Document everything aggressively — future-you will thank present-you when the provider deprecates an endpoint at 3 AM during Dashain sale season.
Moving Forward With Your Custom Gateway
WooCommerce custom payment gateway development unlocks payment methods that directly serve your customers' banking habits rather than forcing them through international processors that add fees and friction. Start with a redirect gateway for your primary Nepal provider, implement rigorous webhook verification from day one, and maintain comprehensive logging before you ever touch production traffic. When the integration grows beyond a single provider or your team lacks bandwidth, bringing in experienced help prevents costly rewrites later. Ready to discuss your specific payment integration needs? Get in touch to review your requirements and timeline.

