WooCommerce Payment Gateway Integration: Step-by-Step Developer Guide 2025

Complete guide to integrating custom payment gateways in WooCommerce. Learn how to build secure payment processors for Stripe, PayPal, and custom providers with real code examples.

Fysal Yaqoob
15 min
#WooCommerce#Payment Gateway#E-commerce#Stripe#PayPal#PHP#WordPress
WooCommerce Payment Gateway Integration: Step-by-Step Developer Guide 2025 - Featured image for E-commerce guide

I've integrated over 30 different payment gateways into WooCommerce stores—from standard processors like Stripe and PayPal to regional providers and custom banking solutions. In this guide, I'll walk you through everything you need to know to build a production-ready WooCommerce payment gateway.

Why Build a Custom Payment Gateway?

You might need a custom payment gateway when:

Regional Payment Providers: Your country uses payment processors not supported by existing plugins Custom Business Logic: You need specific payment flows or approval processes Cost Savings: Avoid paying for premium plugins when you can build it yourself Client Requirements: Specific banking integrations or corporate payment systems Learning: Understanding payment gateway architecture makes you a better developer

I recently built a custom gateway for a Middle Eastern e-commerce client integrating with their local bank's API. The solution saved them $500/year in plugin costs and processed $2M+ in transactions.

Understanding WooCommerce Payment Gateway Architecture

WooCommerce payment gateways extend the WC_Payment_Gateway class and hook into the checkout process at specific points:

  1. Customer selects payment method → Gateway displays payment fields
  2. Customer submits order → Gateway processes payment
  3. Payment success/failure → Gateway updates order status
  4. Admin manages settings → Gateway settings page in WooCommerce

Types of Payment Gateways

Direct/On-Site: Customer enters card details on your site (requires PCI compliance)

  • Examples: Stripe Elements, Authorize.Net
  • Higher conversion rates
  • More development complexity
  • Requires SSL certificate

Redirect/Off-Site: Customer redirected to payment provider

  • Examples: PayPal Standard, 2Checkout
  • Lower conversion rates
  • Easier implementation
  • Payment provider handles PCI compliance

Server-to-Server: Backend API communication

  • Examples: Bank transfers, custom integrations
  • Full control over flow
  • Requires API credentials

Building a Basic Payment Gateway Plugin

Let's build a complete payment gateway from scratch. We'll create a redirect-based gateway similar to PayPal Standard.

Step 1: Create Plugin Structure

Create folder: wp-content/plugins/custom-payment-gateway/

Main plugin file (custom-payment-gateway.php):

<?php
/**
 * Plugin Name: Custom Payment Gateway
 * Plugin URI: https://yoursite.com
 * Description: Custom payment gateway for WooCommerce
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yoursite.com
 * Text Domain: custom-payment-gateway
 * Requires at least: 5.8
 * Requires PHP: 7.4
 * WC requires at least: 6.0
 * WC tested up to: 8.5
 */

// Exit if accessed directly
if (!defined('ABSPATH')) {
    exit;
}

// Check if WooCommerce is active
if (in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {

    // Initialize the gateway
    add_action('plugins_loaded', 'init_custom_payment_gateway');

    function init_custom_payment_gateway() {

        // Include the gateway class
        require_once plugin_dir_path(__FILE__) . 'includes/class-custom-payment-gateway.php';

        // Add gateway to WooCommerce
        add_filter('woocommerce_payment_gateways', 'add_custom_payment_gateway');

        function add_custom_payment_gateway($gateways) {
            $gateways[] = 'WC_Custom_Payment_Gateway';
            return $gateways;
        }

        // Add settings link on plugin page
        add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'custom_gateway_settings_link');

        function custom_gateway_settings_link($links) {
            $settings_link = '<a href="' . admin_url('admin.php?page=wc-settings&tab=checkout&section=custom_payment') . '">Settings</a>';
            array_unshift($links, $settings_link);
            return $links;
        }
    }
}

Step 2: Create the Gateway Class

Create file: includes/class-custom-payment-gateway.php

<?php
/**
 * Custom Payment Gateway Class
 */

class WC_Custom_Payment_Gateway extends WC_Payment_Gateway {

    /**
     * Constructor
     */
    public function __construct() {
        $this->id = 'custom_payment';
        $this->icon = '';
        $this->has_fields = false; // No payment fields on checkout page
        $this->method_title = 'Custom Payment Gateway';
        $this->method_description = 'Accept payments via our custom payment processor';

        // Load settings
        $this->init_form_fields();
        $this->init_settings();

        // Get settings
        $this->title = $this->get_option('title');
        $this->description = $this->get_option('description');
        $this->enabled = $this->get_option('enabled');
        $this->testmode = 'yes' === $this->get_option('testmode');
        $this->api_key = $this->testmode ? $this->get_option('test_api_key') : $this->get_option('live_api_key');
        $this->api_secret = $this->testmode ? $this->get_option('test_api_secret') : $this->get_option('live_api_secret');

        // Hooks
        add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
        add_action('woocommerce_api_' . strtolower(get_class($this)), array($this, 'handle_payment_callback'));
    }

    /**
     * Initialize gateway settings form fields
     */
    public function init_form_fields() {
        $this->form_fields = array(
            'enabled' => array(
                'title'   => 'Enable/Disable',
                'type'    => 'checkbox',
                'label'   => 'Enable Custom Payment Gateway',
                'default' => 'no'
            ),
            'title' => array(
                'title'       => 'Title',
                'type'        => 'text',
                'description' => 'Payment method title that customers see during checkout',
                'default'     => 'Credit Card / Debit Card',
                'desc_tip'    => true,
            ),
            'description' => array(
                'title'       => 'Description',
                'type'        => 'textarea',
                'description' => 'Payment method description that customers see during checkout',
                'default'     => 'Pay securely using your credit or debit card.',
                'desc_tip'    => true,
            ),
            'testmode' => array(
                'title'       => 'Test Mode',
                'label'       => 'Enable Test Mode',
                'type'        => 'checkbox',
                'description' => 'Place the payment gateway in test mode using test API credentials',
                'default'     => 'yes',
                'desc_tip'    => true,
            ),
            'test_api_key' => array(
                'title'       => 'Test API Key',
                'type'        => 'text',
                'description' => 'Get your API credentials from your payment provider dashboard',
                'default'     => '',
                'desc_tip'    => true,
            ),
            'test_api_secret' => array(
                'title'       => 'Test API Secret',
                'type'        => 'password',
                'description' => 'Keep this secret and never share it publicly',
                'default'     => '',
                'desc_tip'    => true,
            ),
            'live_api_key' => array(
                'title'       => 'Live API Key',
                'type'        => 'text',
                'description' => 'Production API key from your payment provider',
                'default'     => '',
                'desc_tip'    => true,
            ),
            'live_api_secret' => array(
                'title'       => 'Live API Secret',
                'type'        => 'password',
                'description' => 'Production API secret - keep this confidential',
                'default'     => '',
                'desc_tip'    => true,
            ),
        );
    }

    /**
     * Process payment
     */
    public function process_payment($order_id) {
        $order = wc_get_order($order_id);

        // Create payment request
        $payment_url = $this->create_payment_request($order);

        if (is_wp_error($payment_url)) {
            wc_add_notice($payment_url->get_error_message(), 'error');
            return array(
                'result' => 'failure'
            );
        }

        // Mark order as pending payment
        $order->update_status('pending', 'Customer redirected to payment gateway');

        // Reduce stock levels
        wc_reduce_stock_levels($order_id);

        // Remove cart
        WC()->cart->empty_cart();

        // Redirect to payment URL
        return array(
            'result'   => 'success',
            'redirect' => $payment_url
        );
    }

    /**
     * Create payment request and return payment URL
     */
    private function create_payment_request($order) {
        $api_url = $this->testmode
            ? 'https://sandbox.payment-provider.com/api/v1/checkout'
            : 'https://payment-provider.com/api/v1/checkout';

        $callback_url = WC()->api_request_url(get_class($this));

        $payment_data = array(
            'merchant_id'   => $this->api_key,
            'amount'        => $order->get_total(),
            'currency'      => $order->get_currency(),
            'order_id'      => $order->get_id(),
            'return_url'    => $this->get_return_url($order),
            'cancel_url'    => $order->get_cancel_order_url(),
            'callback_url'  => $callback_url,
            'customer'      => array(
                'name'  => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
                'email' => $order->get_billing_email(),
                'phone' => $order->get_billing_phone(),
            ),
            'items' => $this->get_order_items($order),
            'timestamp'     => time(),
        );

        // Generate signature (HMAC)
        $payment_data['signature'] = $this->generate_signature($payment_data);

        // Make API request
        $response = wp_remote_post($api_url, array(
            'method'    => 'POST',
            'timeout'   => 45,
            'headers'   => array(
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer ' . $this->api_secret,
            ),
            'body'      => json_encode($payment_data),
        ));

        if (is_wp_error($response)) {
            $this->log('Payment request failed: ' . $response->get_error_message());
            return new WP_Error('api_error', 'Unable to connect to payment provider. Please try again.');
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (isset($body['payment_url'])) {
            return $body['payment_url'];
        }

        $this->log('Invalid API response: ' . print_r($body, true));
        return new WP_Error('invalid_response', 'Payment provider returned an error. Please try again.');
    }

    /**
     * Get order items for payment request
     */
    private function get_order_items($order) {
        $items = array();

        foreach ($order->get_items() as $item) {
            $items[] = array(
                'name'     => $item->get_name(),
                'quantity' => $item->get_quantity(),
                'price'    => $item->get_total(),
            );
        }

        return $items;
    }

    /**
     * Generate signature for payment request
     */
    private function generate_signature($data) {
        // Remove signature field if it exists
        unset($data['signature']);

        // Sort data alphabetically by key
        ksort($data);

        // Create string from data
        $string = http_build_query($data);

        // Generate HMAC SHA256 signature
        return hash_hmac('sha256', $string, $this->api_secret);
    }

    /**
     * Handle payment callback from payment provider
     */
    public function handle_payment_callback() {
        $raw_post = file_get_contents('php://input');
        $data = json_decode($raw_post, true);

        $this->log('Payment callback received: ' . print_r($data, true));

        // Verify signature
        if (!$this->verify_callback_signature($data)) {
            $this->log('Invalid callback signature');
            status_header(403);
            exit('Invalid signature');
        }

        $order_id = isset($data['order_id']) ? intval($data['order_id']) : 0;
        $order = wc_get_order($order_id);

        if (!$order) {
            $this->log('Order not found: ' . $order_id);
            status_header(404);
            exit('Order not found');
        }

        // Check if already processed
        if ($order->is_paid()) {
            status_header(200);
            exit('Order already processed');
        }

        $payment_status = isset($data['status']) ? sanitize_text_field($data['status']) : '';
        $transaction_id = isset($data['transaction_id']) ? sanitize_text_field($data['transaction_id']) : '';

        switch ($payment_status) {
            case 'success':
                $order->payment_complete($transaction_id);
                $order->add_order_note('Payment completed via Custom Payment Gateway. Transaction ID: ' . $transaction_id);
                break;

            case 'failed':
                $order->update_status('failed', 'Payment failed');
                break;

            case 'pending':
                $order->update_status('on-hold', 'Payment pending confirmation');
                break;

            default:
                $this->log('Unknown payment status: ' . $payment_status);
        }

        status_header(200);
        exit('OK');
    }

    /**
     * Verify callback signature
     */
    private function verify_callback_signature($data) {
        if (!isset($data['signature'])) {
            return false;
        }

        $received_signature = $data['signature'];
        $expected_signature = $this->generate_signature($data);

        return hash_equals($expected_signature, $received_signature);
    }

    /**
     * Log messages
     */
    private function log($message) {
        if ($this->testmode) {
            $logger = wc_get_logger();
            $logger->debug($message, array('source' => 'custom-payment-gateway'));
        }
    }
}

Building a Direct Payment Gateway (Stripe-style)

For on-site payments where customers enter card details on your site:

class WC_Direct_Payment_Gateway extends WC_Payment_Gateway {

    public function __construct() {
        // ... basic setup ...
        $this->has_fields = true; // Enable payment fields
        $this->supports = array(
            'products',
            'refunds',
            'subscriptions', // If supporting WooCommerce Subscriptions
        );
    }

    /**
     * Payment fields on checkout page
     */
    public function payment_fields() {
        if ($this->description) {
            echo wpautop(wp_kses_post($this->description));
        }

        ?>
        <fieldset class="wc-payment-form">
            <div class="form-row form-row-wide">
                <label>Card Number <span class="required">*</span></label>
                <input type="text" class="input-text" name="custom_card_number" placeholder="1234 5678 9012 3456" autocomplete="cc-number" />
            </div>

            <div class="form-row form-row-first">
                <label>Expiry Date <span class="required">*</span></label>
                <input type="text" class="input-text" name="custom_card_expiry" placeholder="MM / YY" autocomplete="cc-exp" />
            </div>

            <div class="form-row form-row-last">
                <label>CVV <span class="required">*</span></label>
                <input type="text" class="input-text" name="custom_card_cvv" placeholder="123" autocomplete="cc-csc" maxlength="4" />
            </div>

            <div class="clear"></div>
        </fieldset>
        <?php
    }

    /**
     * Validate payment fields
     */
    public function validate_fields() {
        if (empty($_POST['custom_card_number'])) {
            wc_add_notice('Card number is required', 'error');
            return false;
        }

        if (empty($_POST['custom_card_expiry'])) {
            wc_add_notice('Expiry date is required', 'error');
            return false;
        }

        if (empty($_POST['custom_card_cvv'])) {
            wc_add_notice('CVV is required', 'error');
            return false;
        }

        // Add more validation (Luhn algorithm for card number, expiry date format, etc.)

        return true;
    }

    /**
     * Process payment
     */
    public function process_payment($order_id) {
        $order = wc_get_order($order_id);

        // Get card details (NEVER log or store full card numbers)
        $card_number = sanitize_text_field($_POST['custom_card_number']);
        $card_expiry = sanitize_text_field($_POST['custom_card_expiry']);
        $card_cvv = sanitize_text_field($_POST['custom_card_cvv']);

        // Tokenize or encrypt card data before sending to payment API
        $payment_token = $this->tokenize_card($card_number, $card_expiry, $card_cvv);

        if (is_wp_error($payment_token)) {
            wc_add_notice($payment_token->get_error_message(), 'error');
            return array('result' => 'failure');
        }

        // Process payment via API
        $result = $this->charge_card($order, $payment_token);

        if (is_wp_error($result)) {
            wc_add_notice($result->get_error_message(), 'error');
            return array('result' => 'failure');
        }

        // Mark payment complete
        $order->payment_complete($result['transaction_id']);
        $order->add_order_note('Payment completed. Transaction ID: ' . $result['transaction_id']);

        // Reduce stock
        wc_reduce_stock_levels($order_id);

        // Empty cart
        WC()->cart->empty_cart();

        return array(
            'result'   => 'success',
            'redirect' => $this->get_return_url($order)
        );
    }

    /**
     * Tokenize card (send to payment API to create token)
     */
    private function tokenize_card($number, $expiry, $cvv) {
        // API call to tokenize card
        // NEVER store raw card data

        $response = wp_remote_post('https://api.payment-provider.com/tokens', array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $this->api_secret,
                'Content-Type' => 'application/json',
            ),
            'body' => json_encode(array(
                'card_number' => $number,
                'expiry_date' => $expiry,
                'cvv' => $cvv,
            ))
        ));

        if (is_wp_error($response)) {
            return new WP_Error('tokenization_failed', 'Unable to process card');
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (isset($body['token'])) {
            return $body['token'];
        }

        return new WP_Error('invalid_card', 'Card validation failed');
    }

    /**
     * Charge card via API
     */
    private function charge_card($order, $token) {
        $response = wp_remote_post('https://api.payment-provider.com/charges', array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $this->api_secret,
                'Content-Type' => 'application/json',
            ),
            'body' => json_encode(array(
                'token' => $token,
                'amount' => $order->get_total() * 100, // Amount in cents
                'currency' => $order->get_currency(),
                'description' => 'Order #' . $order->get_id(),
                'metadata' => array(
                    'order_id' => $order->get_id(),
                    'customer_email' => $order->get_billing_email(),
                )
            ))
        ));

        if (is_wp_error($response)) {
            return new WP_Error('charge_failed', 'Payment processing failed');
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (isset($body['status']) && $body['status'] === 'succeeded') {
            return array(
                'transaction_id' => $body['id'],
                'status' => 'success'
            );
        }

        return new WP_Error('payment_declined', $body['error_message'] ?? 'Payment was declined');
    }
}

Implementing Refunds

Add refund support to your gateway:

public function __construct() {
    // ... existing code ...
    $this->supports = array('products', 'refunds');
}

/**
 * Process refund
 */
public function process_refund($order_id, $amount = null, $reason = '') {
    $order = wc_get_order($order_id);

    if (!$order) {
        return new WP_Error('invalid_order', 'Order not found');
    }

    $transaction_id = $order->get_transaction_id();

    if (!$transaction_id) {
        return new WP_Error('no_transaction', 'No transaction ID found');
    }

    // Call payment provider API to process refund
    $response = wp_remote_post('https://api.payment-provider.com/refunds', array(
        'headers' => array(
            'Authorization' => 'Bearer ' . $this->api_secret,
            'Content-Type' => 'application/json',
        ),
        'body' => json_encode(array(
            'transaction_id' => $transaction_id,
            'amount' => $amount * 100, // Amount in cents
            'reason' => $reason,
        ))
    ));

    if (is_wp_error($response)) {
        return new WP_Error('refund_failed', $response->get_error_message());
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);

    if (isset($body['status']) && $body['status'] === 'succeeded') {
        $order->add_order_note(sprintf('Refund of %s processed successfully. Refund ID: %s', wc_price($amount), $body['id']));
        return true;
    }

    return new WP_Error('refund_error', $body['error'] ?? 'Refund failed');
}

Subscription Support

For recurring payments (WooCommerce Subscriptions):

public function __construct() {
    // ... existing code ...
    $this->supports = array(
        'products',
        'subscriptions',
        'subscription_cancellation',
        'subscription_suspension',
        'subscription_reactivation',
        'subscription_amount_changes',
        'subscription_date_changes',
        'multiple_subscriptions',
    );

    // Subscription hooks
    add_action('woocommerce_scheduled_subscription_payment_' . $this->id, array($this, 'process_subscription_payment'), 10, 2);
}

/**
 * Process scheduled subscription payment
 */
public function process_subscription_payment($amount, $order) {
    // Get saved payment token for customer
    $user_id = $order->get_user_id();
    $payment_token = get_user_meta($user_id, '_payment_token', true);

    if (!$payment_token) {
        $order->update_status('failed', 'No payment method saved for subscription');
        return;
    }

    // Process payment
    $result = $this->charge_card($order, $payment_token);

    if (is_wp_error($result)) {
        $order->update_status('failed', $result->get_error_message());
    } else {
        $order->payment_complete($result['transaction_id']);
    }
}

Security Best Practices

1. PCI Compliance

CRITICAL: If you're handling credit card data directly:

  • Never store full card numbers (use tokenization)
  • Never log card details
  • Use HTTPS (SSL certificate required)
  • Validate and sanitize all input
  • Follow PCI DSS standards

Recommended: Use hosted payment pages or JavaScript libraries (like Stripe Elements) that handle card data without it touching your server.

2. Validate All Input

// Sanitize and validate
$order_id = absint($_POST['order_id']);
$amount = floatval($_POST['amount']);
$email = sanitize_email($_POST['email']);

// Verify nonces for admin actions
if (!wp_verify_nonce($_POST['_wpnonce'], 'gateway_action')) {
    wp_die('Security check failed');
}

3. Verify Callbacks

Always verify payment provider callbacks:

private function verify_callback_signature($data) {
    $expected = hash_hmac('sha256', json_encode($data), $this->api_secret);
    return hash_equals($expected, $data['signature']);
}

4. Use WordPress HTTP API

Never use file_get_contents() or cURL directly. Use WordPress HTTP API:

$response = wp_remote_post($url, array(
    'timeout' => 30,
    'headers' => array(
        'Authorization' => 'Bearer ' . $this->api_key
    ),
    'body' => $data
));

if (is_wp_error($response)) {
    // Handle error
}

Testing Your Gateway

Test Mode Setup

Always include test mode:

$this->testmode = 'yes' === $this->get_option('testmode');
$this->api_url = $this->testmode
    ? 'https://sandbox.provider.com/api'
    : 'https://provider.com/api';

Test Checklist

✅ Successful payment ✅ Failed payment ✅ Pending payment ✅ Payment callback handling ✅ Refund processing ✅ Error handling ✅ Order status updates ✅ Customer notifications ✅ Admin notifications ✅ Security (signature verification) ✅ Logging (test mode only)

Test Cards

Most payment providers offer test card numbers:

Visa: 4242424242424242 Mastercard: 5555555555554444 Declined: 4000000000000002

Common Issues

Issue 1: Callback URL Not Working

Solution: Check permalink structure

// Callback URL format
$callback_url = WC()->api_request_url(get_class($this));
// Results in: https://yoursite.com/?wc-api=wc_custom_payment_gateway

Issue 2: Order Status Not Updating

Cause: Callback signature verification failing

Solution: Log callback data and verify signature algorithm matches provider documentation

Issue 3: Payment Stuck in Pending

Cause: Callback not received or processed

Solution: Check logs, verify callback URL is publicly accessible, check for PHP errors

Frequently Asked Questions

Do I need PCI compliance for a redirect gateway?

No, if customers are redirected to the payment provider's site and never enter card details on your site.

Can I test without a payment provider account?

Yes, create a dummy gateway that simulates payment flow without actual API calls.

How do I add custom fields to checkout?

Use payment_fields() method to display custom HTML on checkout page.

Should I use a plugin or build custom?

Use plugin if:

  • Provider has official WooCommerce plugin
  • You need quick setup
  • No custom requirements

Build custom if:

  • Provider has no plugin
  • You need specific customization
  • You're integrating with custom system

Next Steps

Now you can:

  1. Integrate popular providers: Stripe, PayPal, Square
  2. Build regional gateways: Local bank integrations
  3. Add subscription support: Recurring payments
  4. Implement saved cards: Tokenization for repeat customers
  5. Add Apple Pay/Google Pay: Digital wallet support

Resources


Need help integrating a custom payment gateway? I've built 30+ WooCommerce payment integrations for clients worldwide. Contact me for custom development.

Fysal Yaqoob

Fysal Yaqoob

Expert WordPress & Shopify Developer

Senior full-stack developer with 10+ years experience specializing in WordPress, Shopify, and headless CMS solutions. Delivering custom themes, plugins, e-commerce stores, and scalable web applications.

10+ Years500+ Projects100+ Agencies

Practice: Code Typer

Master WooCommerce development by practicing with real code snippets in Code Typer!

Easy3-5 min
Play Now