WooCommerce payment gateways: a practical WordPress guide
Payment processing is the heartbeat of any e-commerce operation, and getting it right in WooCommerce determines whether customers complete purchases or abandon carts. Building or customizing a payment gateway might sound complex, but with the right foundation and tested patterns, you can integrate new processors, add custom logic, and handle asynchronous payment flows reliably. This guide walks through the essentials we've learned integrating everything from Stripe and PayPal alternatives to regional processors in high-volume stores.
Extend WC_Payment_Gateway
Every payment gateway in WooCommerce inherits from WC_Payment_Gateway, which handles settings registration, payment form rendering, and order status updates. This class is your contract with WooCommerce; implement its required methods and you unlock the full ecosystem.
Start by registering your gateway class. WooCommerce discovers gateways through a filter hook:
add_filter( 'woocommerce_payment_gateways', function( $gateways ) {
$gateways[] = 'WC_Gateway_Custom_Processor';
return $gateways;
} );Your gateway class needs at least these methods:
class WC_Gateway_Custom_Processor extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'custom_processor';
$this->method_title = 'Custom Processor';
$this->method_description = 'Accept payments via Custom Processor';
$this->has_fields = false;
$this->supports = array( 'products', 'refunds' );
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
$this->api_key = $this->get_option( 'api_key' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id,
array( $this, 'process_admin_options' ) );
}
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => 'Enable/Disable',
'type' => 'checkbox',
'label' => 'Enable Custom Processor',
'default' => 'yes',
),
'title' => array(
'title' => 'Title',
'type' => 'text',
'description' => 'What customers see at checkout',
'default' => 'Custom Processor',
),
'api_key' => array(
'title' => 'API Key',
'type' => 'password',
'description' => 'Your API key from Custom Processor',
),
);
}
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
try {
$response = $this->charge_card( $order );
if ( $response['success'] ) {
$order->payment_complete( $response['transaction_id'] );
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order ),
);
} else {
wc_add_notice( 'Payment failed: ' . $response['message'], 'error' );
return array( 'result' => 'fail' );
}
} catch ( Exception $e ) {
wc_add_notice( 'Payment error: ' . $e->getMessage(), 'error' );
return array( 'result' => 'fail' );
}
}
private function charge_card( $order ) {
// API call to your processor
$response = wp_remote_post( 'https://api.customprocessor.com/charge', array(
'body' => wp_json_encode( array(
'amount' => $order->get_total(),
'currency' => get_woocommerce_currency(),
'description' => 'Order #' . $order->get_id(),
) ),
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
),
'timeout' => 30,
) );
if ( is_wp_error( $response ) ) {
throw new Exception( $response->get_error_message() );
}
return json_decode( wp_remote_retrieve_body( $response ), true );
}
}The process_payment method is your workhorse—it charges the card synchronously, updates the order, and returns success or failure. For async processors, you'll handle this differently, which we cover next.
Support blocks checkout
The modern WooCommerce checkout is built on blocks, and payments must integrate cleanly. Rather than relying on the legacy shortcode checkout, you need to register your gateway for the Checkout block.
WooCommerce provides a REST endpoint that blocks checkout communicates with. To make your gateway available there, register it via the payment methods registry in JavaScript. First, ensure your gateway declares support in the constructor:
$this->supports = array( 'products', 'refunds' );Then create a JavaScript file that registers the payment method:
// In your gateway class init:
wp_register_script(
'wc-custom-processor',
plugin_dir_url( __FILE__ ) . 'payment-method.js',
array( 'wc-blocks-checkout' ),
'1.0.0',
true
);
add_action( 'woocommerce_blocks_payment_method_type_registration', function( $payment_methods ) {
$payment_methods->register( new WC_Custom_Processor_Blocks() );
} );Create a Blocks integration class:
class WC_Custom_Processor_Blocks implements Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface {
private $gateway;
public function __construct() {
$this->gateway = new WC_Gateway_Custom_Processor();
}
public function get_name() {
return 'custom_processor';
}
public function get_payment_method_script_handles() {
return array( 'wc-custom-processor' );
}
public function get_payment_method_data() {
return array(
'title' => $this->gateway->get_option( 'title' ),
'description' => $this->gateway->get_option( 'description' ),
'supports' => $this->gateway->supports,
);
}
}On the frontend, your JavaScript payment method handles submission and error states. Blocks checkout sends the payment details via REST, so ensure your endpoint validates and processes correctly.
Handle webhooks for async payments
Not all payments complete synchronously. Services like Stripe or regional processors often require you to poll for status or listen to webhooks. Webhook handling is critical—a missing or failed webhook means a customer paid but their order status never updates.
Register a REST endpoint that your processor calls:
add_action( 'rest_api_init', function() {
register_rest_route( 'custom-processor/v1', '/webhook', array(
'methods' => 'POST',
'callback' => array( $this, 'handle_webhook' ),
'permission_callback' => '__return_true', // Verify signature instead
) );
} );In the webhook handler, always verify the signature and idempotency:
public function handle_webhook( WP_REST_Request $request ) {
$body = $request->get_body();
$signature = $request->get_header( 'X-Signature' );
// Verify webhook authenticity
$expected_signature = hash_hmac( 'sha256', $body, $this->api_secret );
if ( ! hash_equals( $signature, $expected_signature ) ) {
return new WP_REST_Response( 'Invalid signature', 401 );
}
$event = json_decode( $body, true );
// Check if we've already processed this event
$processed = get_transient( 'cp_webhook_' . $event['id'] );
if ( $processed ) {
return new WP_REST_Response( 'Already processed', 200 );
}
if ( 'payment.completed' === $event['type'] ) {
$order = wc_get_order_by_order_key( $event['order_key'] );
if ( $order ) {
$order->payment_complete( $event['transaction_id'] );
$order->add_order_note( 'Payment confirmed via webhook.' );
}
set_transient( 'cp_webhook_' . $event['id'], true, HOUR_IN_SECONDS );
}
return new WP_REST_Response( 'OK', 200 );
}The transient check prevents duplicate processing if the webhook is sent twice. Always store the webhook event ID or a hash to verify idempotency. In production, consider logging all webhooks to a custom table for audit trails.
Test with sandbox accounts
Before going live, you need a safe sandbox environment. Most payment processors provide test credentials that return predictable responses without charging real money.
Add a test mode toggle to your gateway settings:
'test_mode' => array(
'title' => 'Test Mode',
'type' => 'checkbox',
'label' => 'Enable test mode (uses sandbox API)',
'default' => 'yes',
),Then switch endpoints based on this setting:
private function get_api_endpoint() {
$test_mode = 'yes' === $this->get_option( 'test_mode' );
if ( $test_mode ) {
return 'https://sandbox-api.customprocessor.com';
}
return 'https://api.customprocessor.com';
}Use test card numbers provided by your processor. Common patterns include cards ending in certain numbers that trigger specific responses: declined, fraud, etc. Document these in a development wiki so your team knows which card to use for which scenario.
Test order workflows end-to-end: successful payment, failed payment, refund, webhook retry, and timeout scenarios. Use WP-CLI to create test orders in bulk if needed.
Common mistakes
- Not validating webhook signatures. Any attacker can forge webhooks. Always verify the signature using your API secret.
- Missing idempotency on webhooks. Webhooks can retry; without deduplication, an order might be marked paid twice or refunded multiple times.
- Storing raw credit card data. This violates PCI compliance. Use tokenization APIs and store only tokens.
- Assuming synchronous payment always succeeds. Network timeouts happen. Log responses and implement retry logic with exponential backoff.
- Forgetting to test the blocks checkout path. If you only test the legacy shortcode, the new checkout silently fails for users.
- Hardcoding API credentials. Always use WordPress options or environment variables; never commit keys to the repository.
Key takeaways
- Extend
WC_Payment_Gateway, implementprocess_payment, and register via thewoocommerce_payment_gatewaysfilter to create a functioning gateway. - Blocks checkout requires a separate JavaScript registration and a Blocks integration class; don't assume legacy shortcode support is enough.
- Handle async payments via webhooks, always verify signatures, and use transients or a log table to ensure idempotency.
- Test thoroughly in sandbox mode, documenting test card numbers and error scenarios your team will encounter.
- Prioritize security: validate input, store tokens not cards, and protect your API keys.






Comments
Loading comments…