Create a WooCommerce Payment Gateway Plugin from Scratch

A WooCommerce payment gateway plugin extends the abstract WC_Payment_Gateway class and must implement at minimum: a constructor that sets the gateway ID, title, icon, description, and supported features; an init_form_fields() method that defines the admin settings form; an init_settings() call in the constructor to load saved settings; and a process_payment() method that handles the actual payment and returns an array with result and redirect keys. The gateway is registered by hooking into woocommerce_payment_gateways and appending the class name to the array. The process_payment() method receives the order ID, creates the WooCommerce order object with wc_get_order(), calls the payment provider API, and on success marks the order as paid with $order->payment_complete() — which transitions the order status to “Processing” and sends the customer order confirmation email. On failure it adds a WooCommerce notice with wc_add_notice() and returns ['result' => 'failure']. The gateway admin settings defined in init_form_fields() include fields for API key, API secret, sandbox mode toggle, and title — all settings are read back with $this->get_option('api_key') after init_settings(). The API key and secret are stored in the WordPress options table via the standard WooCommerce settings save mechanism — they are encrypted at rest only if the host uses filesystem-level encryption, so production deployments should use environment variables or a secrets manager rather than storing credentials in the database. Webhooks from the payment provider are received via a custom REST route or the legacy WC_Payment_Gateway::check_ipn_response() pattern — always verify the webhook signature before processing to prevent forged payment confirmations. The rate limiting post applies directly to the payment endpoint — rate-limit the checkout page to prevent credential stuffing attacks against saved payment methods.

Problem: A WooCommerce store needs to integrate with a regional payment provider that has no existing plugin — the integration must support sandbox and production modes, store API credentials in the admin panel, and handle payment success and failure responses correctly.

Solution: Extend WC_Payment_Gateway, define admin settings fields for API credentials and sandbox mode, implement process_payment() to call the provider API and return success or failure, and verify webhook signatures before updating order status.

// Register the gateway
add_filter('woocommerce_payment_gateways', function(array $gateways): array {
    $gateways[] = 'Myplugin_Payment_Gateway';
    return $gateways;
});

// Load the class after WooCommerce is ready
add_action('plugins_loaded', function() {
    if (!class_exists('WC_Payment_Gateway')) return;

    class Myplugin_Payment_Gateway extends WC_Payment_Gateway {

        public function __construct() {
            $this->id                 = 'myplugin_gateway';
            $this->method_title       = __('My Payment Provider', 'myplugin');
            $this->method_description = __('Pay via My Payment Provider.', 'myplugin');
            $this->supports           = ['products'];

            $this->init_form_fields();
            $this->init_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');

            add_action('woocommerce_update_options_payment_gateways_' . $this->id,
                       [$this, 'process_admin_options']);
        }

        public function init_form_fields(): void {
            $this->form_fields = [
                'enabled'      => ['title' => __('Enable', 'myplugin'),   'type' => 'checkbox', 'default' => 'no'],
                'title'        => ['title' => __('Title', 'myplugin'),    'type' => 'text',     'default' => __('Credit Card', 'myplugin')],
                'description' => ['title' => __('Description', 'myplugin'), 'type' => 'textarea', 'default' => ''],
                'testmode'     => ['title' => __('Sandbox mode', 'myplugin'), 'type' => 'checkbox', 'default' => 'yes'],
                'test_api_key' => ['title' => __('Test API Key', 'myplugin'), 'type' => 'password', 'default' => ''],
                'live_api_key' => ['title' => __('Live API Key', 'myplugin'), 'type' => 'password', 'default' => ''],
            ];
        }

        public function process_payment(int $order_id): array {
            $order = wc_get_order($order_id);
            if (!$order) return ['result' => 'failure'];

            // Call payment provider API
            $response = wp_remote_post('https://api.myprovider.com/charge', [
                'body'    => json_encode([
                    'amount'   => (int)round($order->get_total() * 100), // cents
                    'currency' => strtolower(get_woocommerce_currency()),
                    'order_id' => $order_id,
                ]),
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->api_key,
                    'Content-Type'  => 'application/json',
                ],
                'timeout' => 15,
            ]);

            if (is_wp_error($response)) {
                wc_add_notice(__('Payment connection failed. Please try again.', 'myplugin'), 'error');
                return ['result' => 'failure'];
            }

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

            if (isset($body['success']) && $body['success']) {
                $order->payment_complete($body['transaction_id'] ?? '');
                $order->add_order_note(sprintf(__('Payment completed. Transaction: %s', 'myplugin'), $body['transaction_id'] ?? ''));
                WC()->cart->empty_cart();
                return ['result' => 'success', 'redirect' => $this->get_return_url($order)];
            }

            $error = sanitize_text_field($body['error'] ?? __('Payment declined.', 'myplugin'));
            wc_add_notice($error, 'error');
            return ['result' => 'failure'];
        }
    }
});

NOTE: Never log full API responses that contain card numbers, CVV codes, or authentication tokens — most payment provider APIs redact this data in responses, but verify their documentation. Store only the transaction ID, order amount, and status in WooCommerce order notes and metadata. For PCI DSS compliance, do not store raw card data at any point in your plugin; offload card capture entirely to the provider’s hosted fields or tokenization API.