Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

WooCommerce Custom Payment Gateway Development

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.

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.

Plugin Directory Structuremy-gateway.php(Main Plugin File)includes/class-wc-gateway-custom.phpassets/js/checkout.js, css/gateway.cssKey Responsibilities• Main file: Plugin header, WC version check• Gateway class: Extends WC_Payment_Gateway• Admin fields: API keys, test mode toggle• Process payment: API call + order update• Webhook handler: Signature verification• Assets: Checkout UI enhancements• Logging: wc_get_logger() for debugging• Security: Nonce checks, capability validation
Standard directory layout for WooCommerce custom payment gateway development ensuring upgrade safety and clean separation of concerns

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.

Payment Processing & Webhook FlowCustomerWooCommercePayment ProviderWebhook Endpoint1. Checkout2. Redirect3. User pays4. POST webhook5. Verify sig6. Update order status7. Confirmation emailCritical Implementation Notes• Store transaction ID in order meta BEFORE redirecting user• Verify webhook signature using provider's public key or HMAC secret• Use wc_get_logger() to record all webhook payloads for debugging• Return HTTP 200 immediately after valid signature check
Asynchronous payment flow for WooCommerce custom payment gateway development showing webhook verification and order status updates

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.

CriteriaRedirect GatewayEmbedded / iframe Gateway
User ExperienceLeaves site, returns via callbackStays on checkout page throughout
Implementation ComplexityLow — simple URL redirectHigh — JS SDK, CORS, tokenization
PCI ScopeMinimal — provider handles card dataExpanded — your domain touches payment UI
Mobile ConversionLower — app switching causes drop-offHigher — seamless in-app experience
Nepal Provider SupporteSewa, Khalti, ConnectIPS (standard)Limited — mostly international gateways
Debugging DifficultyEasier — clear redirect boundariesHarder — JS errors, iframe restrictions
Best ForLocal banks, UPI, wallet paymentsCredit 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.

  1. Enable test mode first: Set test_mode = yes in gateway settings and use sandbox API keys. Verify the plugin switches endpoints correctly by checking logs.
  2. Create test products: Use low-value items (Rs 10–50) to avoid accidental real charges. Configure shipping zones to match your target market.
  3. Tunnel webhooks locally: Run ngrok http 8080 and register the HTTPS tunnel URL in your provider's dashboard. Update it each time ngrok restarts unless you pay for fixed subdomains.
  4. 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.
  5. Simulate failure states: Test expired tokens, insufficient funds, network timeouts, and invalid signatures. Happy-path testing alone misses 80% of production bugs.
  6. Verify order notes: Confirm that successful payments add descriptive notes with provider references. Customers and support staff rely on these for dispute resolution.
Safe Testing WorkflowLocal WordPressTest Mode EnabledHTTPS tunnelngrok / CF TunnelPublic HTTPS URLRegister URLSandbox APITest CredentialsWebhook POSTForward to localProcess + LogDebug Checklist✓ Check WooCommerce → Status → Logs for gateway entries✓ Verify webhook payload matches provider documentation✓ Confirm order status transitions: pending → processing✓ Test failure scenarios: timeout, bad signature, duplicate✓ Validate customer receives correct confirmation email
End-to-end testing cycle for WooCommerce custom payment gateway development using sandbox APIs and local tunneling

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.

Frequently Asked Questions

It is creating a dedicated WordPress plugin that integrates a specific payment provider API directly into the WooCommerce checkout flow, bypassing standard plugins to support local methods like eSewa, Khalti, or bank transfers not covered by existing extensions.

Development typically ranges from Rs 45,000 to Rs 120,000 (USD 335–890) depending on API complexity and security requirements. This covers coding, sandbox testing, PCI compliance checks, and deployment, excluding annual maintenance or gateway transaction fees charged by the provider.

Build custom only when no maintained plugin supports your specific provider, transaction flow, or settlement currency. If a verified, updated plugin exists for eSewa or Khalti, use it. Custom development is justified for proprietary banking APIs, unique split-payment logic, or integrating legacy Nepali financial systems lacking modern WooCommerce support.

You must extend WC_Payment_Gateway and implement process_payment() for transaction handling, admin_options() for backend settings, and payment_fields() for checkout UI. Register the gateway via woocommerce_payment_gateways filter. Use woocommerce_thankyou and woocommerce_order_status_changed for post-payment callbacks. Missing any of these breaks the checkout flow or prevents proper order status updates after successful transactions.

Never trust incoming webhook data blindly. Verify signature headers using the provider's secret key before processing. Log raw payloads to a secure file for debugging but never expose them publicly. Update order status only after validation matches expected amounts and order IDs. Implement idempotency keys to prevent duplicate processing if the gateway retries delivery. On client projects, I have seen merchants lose money because developers skipped signature verification and accepted spoofed success notifications.

Yes, if your code touches raw card data. Most Nepali gateways like eSewa and ConnectIPS use redirect or iframe flows where card data stays on their servers, reducing your scope to SAQ A. However, if you collect PAN or CVV fields directly in WooCommerce forms, you inherit full PCI DSS responsibility. Always prefer hosted payment fields or tokenization APIs to minimize compliance burden and security risk on your server.

Use the provider's sandbox environment with test credentials and simulated success/failure responses. Create separate WooCommerce test orders to verify both happy paths and error handling. Mock webhook endpoints locally using tools like ngrok to simulate async notifications. Never test against production APIs. In my experience building integrations for Nepal Gift Card, thorough sandbox testing prevented live transaction failures during initial launch.

Only if the gateway provides tokenization and you store the reference token, never raw card numbers. Save tokens in the wp_wc_customer_tokens table using WooCommerce's built-in token API. Encrypt sensitive metadata at rest. Ensure your database backups are encrypted and access-controlled. Storing raw PAN or CVV violates PCI standards and creates massive liability. For recurring billing on legal-tech portals, I always rely on provider-side token vaults rather than local storage.

This usually means the gateway class isn't registered correctly or fails initialization. Check that your plugin file includes the wc_add_gateway function hooked to woocommerce_payment_gateways. Verify PHP syntax errors aren't silently failing during class load. Confirm currency and country restrictions in your gateway settings match the cart contents. Enable WP_DEBUG_LOG to catch fatal errors. I've debugged this repeatedly on client sites where a missing comma in the config array broke the entire payment method list.

Define supported currencies in your gateway's __construct() method and validate against WC()->cart->get_currency() before rendering. Convert amounts using the gateway's exchange rate API or fixed rates stored in settings. Display converted totals clearly on checkout to avoid customer confusion. Some Nepali banks settle only in NPR despite displaying USD prices. On international florist sites like Petals Nepal, I implemented dual-currency logic to show USD to customers while settling NPR with the local bank.

The most common cause is incorrect order ID mapping between WooCommerce and the gateway callback. Ensure your process_payment() stores the gateway transaction ID in order meta and your webhook handler retrieves the correct order using that ID. Check that order status transitions follow WooCommerce rules; you cannot move from completed back to processing. Verify webhook URL is accessible and not blocked by firewall. Debug by logging order ID lookups during callback execution to identify mismatches.

Override the payment_fields() method in your gateway class to output HTML instructions above the payment form. Use wc_get_template() to keep markup consistent with WooCommerce styling. Store dynamic instructions like account numbers in gateway settings so admins can update them without code changes. Sanitize all output with esc_html() to prevent XSS. For legal service portals requiring advance bank deposits, I use this pattern to display rotating account details and QR codes that update based on selected service type.

Poorly coded gateways can block checkout rendering and hurt LCP/INP metrics. Load gateway scripts conditionally only on checkout pages using is_checkout(). Defer non-critical JS and inline critical CSS for payment forms. Avoid synchronous API calls during page render; use AJAX for real-time validation. Minimize DOM manipulation in payment_fields(). On high-traffic eCommerce sites, I've reduced checkout INP by 200ms simply by moving gateway initialization off the main thread and lazy-loading provider SDKs.

Pin your plugin to specific WooCommerce versions in the header and test against beta releases before major updates. Monitor deprecated hook notices in debug logs. Subscribe to WooCommerce developer changelogs for breaking changes in payment gateway APIs. Maintain a staging environment mirroring production for upgrade testing. Budget annual maintenance hours for compatibility work. Custom gateways break more often than core after updates because they rely on internal APIs that change without deprecation cycles. Plan for this in client contracts.

Deliver setup guides covering API credential configuration, webhook URL registration, and sandbox-to-production switch procedures. Include troubleshooting checklists for common failures like signature mismatches or timeout errors. Document all custom settings fields and their expected values. Provide runbooks for testing payments and verifying order status updates. List dependencies and version requirements. For legal-tech clients managing their own portals, I create video walkthroughs showing how to rotate API keys and interpret transaction logs without developer intervention.

Share this article

Quick Contact Options
Choose how you want to connect me: