Building Custom Payment Gateway Plugins for WooCommerce: Security, Webhooks, and Error Logging
Custom payment gateways let a store integrate a processor or bank that has no maintained plugin. Learn how to engineer a secure WooCommerce payment plugin from scratch.

Most stores should use an official, maintained gateway plugin such as Stripe, PayPal or WooPayments. A custom gateway plugin makes sense when a business must integrate a processor, acquiring bank or regional payment method that has no maintained WooCommerce plugin, or when the checkout flow needs control an off-the-shelf plugin cannot provide.
However, payment processing is a high-risk system. Implementing poor webhook verification or weak hashing signatures exposes your store to checkout exploits, where users can forge transaction success states to download products without actually paying.
This guide provides a secure blueprint for building a custom WooCommerce payment gateway plugin, focusing on webhook authorization, transaction validation, and error logging.
1. Structure of a WooCommerce Gateway Class
Every WooCommerce payment gateway must extend the base WC_Payment_Gateway class. This class registers your gateway settings, handles payment forms, processes the redirect to the bank endpoint, and listens for validation webhooks.
Create your main plugin file (e.g., custom-ipg-gateway.php) and register the custom class during the init phase:
`php
<?php
/*
Plugin Name: WooCommerce Custom IPG Gateway
Description: A secure custom payment integration for WooCommerce.
Version: 1.0.0
Author: Anushka Dahanayake
*/
add_action( 'plugins_loaded', 'init_custom_ipg_gateway_class' );
// Tell WooCommerce the gateway exists.
add_filter( 'woocommerce_payment_gateways', function ( $gateways ) {
$gateways[] = 'WC_Gateway_Custom_IPG';
return $gateways;
} );
function init_custom_ipg_gateway_class() {
// Check inside the hook: WooCommerce may load after this plugin.
if ( ! class_exists( 'WC_Payment_Gateway' ) ) {
return;
}
class WC_Gateway_Custom_IPG extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'custom_ipg';
$this->icon = apply_filters( 'woocommerce_custom_ipg_icon', '' );
$this->has_fields = false;
$this->method_title = __( 'Custom IPG', 'wc-custom-ipg' );
$this->method_description = __( 'Redirects clients to secure local payment gateway.', 'wc-custom-ipg' );
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
$this->merchant_id = $this->get_option( 'merchant_id' );
$this->secret_key = $this->get_option( 'secret_key' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
add_action( 'woocommerce_api_wc_gateway_custom_ipg', array( $this, 'check_ipn_response' ) );
}
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'wc-custom-ipg' ),
'type' => 'checkbox',
'label' => __( 'Enable Custom IPG', 'wc-custom-ipg' ),
'default' => 'no'
),
'title' => array(
'title' => __( 'Title', 'wc-custom-ipg' ),
'type' => 'text',
'default' => __( 'Secure Card Payment', 'wc-custom-ipg' ),
),
'merchant_id' => array(
'title' => __( 'Merchant ID', 'wc-custom-ipg' ),
'type' => 'text',
),
'secret_key' => array(
'title' => __( 'API Secret Key', 'wc-custom-ipg' ),
'type' => 'password',
)
);
}
}
}
`
2. Handling the Checkout Redirect
When the user clicks "Place Order", the process_payment() method is triggered. Instead of handling credit card fields locally (which puts your whole site in scope for the most demanding PCI DSS requirements), your plugin should compile transaction parameters and redirect the user to the provider's hosted payment page. For cards issued in the EU/EEA and UK, that hosted page must handle Strong Customer Authentication under PSD2 (normally 3-D Secure 2), so confirm the provider supports it before you build.
`php
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
// Compile redirect query parameters
$payment_url = 'https://pay.example-provider.com/checkout';
$query_args = array(
'merchant' => $this->merchant_id,
'order_id' => $order_id,
'amount' => $order->get_total(),
'currency' => $order->get_currency(),
'return_url' => $this->get_return_url( $order ),
'cancel_url' => $order->get_cancel_order_url(),
// Signature to prevent parameter tampering. Use the exact
// algorithm and field order your provider documents.
'hash' => hash_hmac( 'sha256', $this->merchant_id . $order_id . $order->get_total() . $order->get_currency(), $this->secret_key )
);
return array(
'result' => 'success',
'redirect' => add_query_arg( $query_args, $payment_url )
);
}
`
3. Securing Webhook Listeners (Instant Payment Notifications - IPN)
When a transaction succeeds, the payment gateway sends a POST request back to your server's webhook endpoint (configured via the woocommerce_api_wc_gateway_custom_ipg hook registered in your constructor).
[!WARNING]
Never update order status to "Completed" based on user-facing query redirects. Always require a signed backend-to-backend webhook payload containing validation parameters.
Secure Webhook Verification Endpoint:
`php
public function check_ipn_response() {
$field = function ( $key ) {
return isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
};
$merchant_id = $field( 'merchant_id' );
$order_id = absint( $field( 'order_id' ) );
$status_code = $field( 'status_code' );
$amount = $field( 'amount' );
$currency = $field( 'currency' );
$hash = $field( 'hash' );
// Step 1: Recreate the signature and compare in constant time
$local_hash = hash_hmac( 'sha256', $merchant_id . $order_id . $amount . $currency . $status_code, $this->secret_key );
if ( $merchant_id !== $this->merchant_id || ! hash_equals( $local_hash, strtolower( $hash ) ) ) {
// Log unauthorized attempt and exit
$this->log_gateway_error( "Hash mismatch. Potential fraud payload from IP: " . $_SERVER['REMOTE_ADDR'] );
status_header( 403 );
exit;
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
$this->log_gateway_error( "Order ID {$order_id} not found." );
status_header( 404 );
exit;
}
// Idempotency: a retried callback for a paid order is a confirmation, not a new payment
if ( $order->is_paid() ) {
status_header( 200 );
exit;
}
// Step 2: Verify amount and currency match the order exactly
if ( wc_format_decimal( $amount, 2 ) !== wc_format_decimal( $order->get_total(), 2 ) || $currency !== $order->get_currency() ) {
$this->log_gateway_error( "Amount mismatch for Order {$order_id}. Pay: {$amount}, Order: " . $order->get_total() );
$order->update_status( 'on-hold', __( 'Payment amount validation mismatch.', 'wc-custom-ipg' ) );
status_header( 400 );
exit;
}
// Step 3: Update Order Status
if ( $status_code === '2' ) { // Success code from the provider's docs
$order->payment_complete( $field( 'transaction_id' ) );
$order->add_order_note( __( 'IPN Validation Success. Card Charged.', 'wc-custom-ipg' ) );
status_header( 200 );
exit;
} else {
$order->update_status( 'failed', __( 'Transaction declined by bank API.', 'wc-custom-ipg' ) );
status_header( 200 );
exit;
}
}
`
4. Professional Error Logging
To troubleshoot payment errors and audit disputed logs, implement a helper logging method using WooCommerce's native WC_Logger class:
`php
private function log_gateway_error( $message ) {
if ( class_exists( 'WC_Logger' ) ) {
$logger = wc_get_logger();
$logger->error( $message, array( 'source' => 'custom-ipg-gateway' ) );
}
}
`
With the default file handler, entries are written under wp-content/uploads/wc-logs/ and can be reviewed under WooCommerce > Status > Logs.
By isolating your webhook endpoints, verifying signatures, validating amounts and maintaining logs, you close the most common ways a forged callback can mark an unpaid order as paid.
Supporting the Block-Based Checkout
The Cart and Checkout blocks are the default for new WooCommerce stores. A gateway built only on WC_Payment_Gateway appears in the classic shortcode checkout but not in the block checkout. To support it, register a payment method integration that extends Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType on the woocommerce_blocks_payment_method_type_registration hook, and ship a small script that calls registerPaymentMethod from @woocommerce/blocks-registry (the wc.wcBlocksRegistry global). The WooCommerce payment method integration docs cover the details. Test both checkout types if the store could use either.
Production Security Checklist
A payment gateway plugin should be treated as financial infrastructure. Before launch, review:
- Secret keys are stored in protected settings and never printed.
- Webhook signatures are verified server-side.
- Order amount, currency and merchant ID are checked.
- Duplicate webhook events are idempotent.
- Failed, cancelled, pending and refunded states are handled.
- User-facing return URLs never mark payment as successful alone.
- Logs avoid full card, customer or secret data.
- Admin settings require proper capability checks.
- Test mode and live mode cannot be confused.
- Refund and reconciliation workflows are documented.
For any gateway, confirm the exact signature formula from the provider and test with real sandbox callbacks, not only browser redirects.
Operational Testing
Run test orders for successful payment, failed payment, cancelled payment, duplicate webhook, wrong amount, wrong currency, missing order, delayed callback and refund. Reconcile WooCommerce order status with gateway dashboard records and settlement exports.
Payment reliability is not only code correctness. It includes support scripts, finance checks, customer communication and incident handling when a provider is delayed.
Frequently Asked Questions
Should a WooCommerce payment plugin collect card details directly?
Usually no. Redirect or hosted-field approaches reduce PCI scope because the gateway handles sensitive card entry. Direct card handling requires much stricter compliance.
Can an order be completed from the return URL?
No. The return URL is controlled by the user's browser and can be replayed or manipulated. Complete orders only after trusted server-side verification.
What is idempotency in payment webhooks?
It means repeated callbacks for the same transaction do not create duplicate payments, duplicate emails or conflicting order states.
What should payment logs include?
Include order ID, gateway reference, status, validation result and bounded error context. Avoid secrets and sensitive payment data.
Order State Design
Map every gateway response to a WooCommerce order state before writing code. Common states include pending payment, processing, completed, failed, cancelled, refunded and on-hold. Some gateways also return pending review, 3D Secure challenge, bank timeout or settlement delay.
Do not collapse every non-success result into failed. A pending bank transfer, fraud review or delayed callback may need a different operational response. The customer email, staff alert and admin note should explain what happened without exposing sensitive gateway data.
Reconciliation and Support
Payment plugins need reconciliation because providers, stores and analytics can disagree. Store the gateway transaction reference, order ID, amount, currency, status, verification timestamp and callback ID. That allows finance or support to compare WooCommerce with the gateway dashboard.
Create a support workflow for charged-but-not-completed orders, completed-but-not-settled orders, duplicate callbacks and customer disputes. The plugin should make these cases visible instead of hiding them inside vague logs.
Final Recommendation
Build custom payment gateways only when the business need is real and the security process is mature. Hosted payment pages, signed webhooks, idempotent state changes, careful logging and reconciliation are the minimum standard.
100-Point Payment Gateway Readiness Score
Score the plugin before live payments:
| Area | Points |
|---|---|
| Hosted or compliant payment entry | 10 |
| Signature verification | 15 |
| Amount and currency validation | 15 |
| Idempotent webhook handling | 10 |
| Complete order-state mapping | 10 |
| Secure settings storage | 10 |
| Bounded logs without secrets | 10 |
| Refund and dispute workflow | 10 |
| Sandbox and live test orders | 5 |
| Reconciliation documentation | 5 |
Any score below 85 should block launch. Payment bugs create direct financial, legal and customer trust risk.
Maintenance Requirements
After launch, review gateway API changes, WooCommerce updates, PHP version changes, webhook failures, fraud attempts, abandoned checkout reports and support tickets. Payment integrations should be monitored permanently, especially when plugins, themes or checkout settings change.
Example Failure Case
A customer pays successfully at the bank, but the callback reaches WooCommerce late. If the plugin only trusts the browser return page, the order may be completed without proof or marked failed even though money was captured. A mature gateway plugin stores the pending order, waits for a signed webhook, records the gateway reference and gives staff a clear reconciliation path.
Another common case is duplicate callbacks. A gateway may retry the same notification several times. Without idempotency, the store can send duplicate emails, reduce stock twice or create confusing order notes. The plugin should recognize the transaction reference and treat repeated valid callbacks as confirmations, not new payment events.
These details are why payment code must be designed around failure, not only the happy path.
Postlaunch Monitoring Plan
For the first 30 days, review gateway logs daily. Track failed payments, pending orders, webhook failures, duplicate callbacks, order notes, refund attempts and customer support messages. Compare total paid orders in WooCommerce with the gateway dashboard and settlement exports.
After the first month, move to weekly reviews unless transaction volume is high. Keep alerts active for signature failures, amount mismatches and repeated webhook errors. These signals can reveal configuration mistakes, provider outages or attempted fraud.
Build Versus Buy
Use an official gateway plugin when it supports the business flow, currency, settlement process and checkout experience. Build custom only when the official option is missing, unreliable or unable to support a required payment workflow.
Custom code creates long-term responsibility. The business must maintain provider API changes, security updates, WooCommerce compatibility, support workflows and logs. If that ownership is not available, a supported plugin or hosted checkout is usually safer.
For secure implementation, connect this work with WooCommerce Store Setup, Checkout Optimization, and Website Maintenance.
Related posts

Shared vs VPS vs Managed Hosting for a Small Business Website or Store
A plain comparison of shared hosting, VPS and managed hosting or PaaS for small business sites and online stores: responsibilities, isolation and performance, the signs a WooCommerce store or Next.js app has outgrown shared hosting, GDPR data residency, and a decision table.
Read article →

How to Secure a New Ubuntu VPS: A Setup Checklist for Business Websites
A step-by-step hardening checklist for a fresh Ubuntu 26.04 or 24.04 LTS VPS that will host a business website, with copy-paste commands for SSH keys, ufw, unattended-upgrades, fail2ban, time sync, swap, monitoring and backups.
Read article →

Deploy a Next.js 16 App on a VPS with Nginx, systemd or PM2, and HTTPS
A working guide to running Next.js 16 on your own VPS: Node.js LTS, build-time versus runtime environment variables, a systemd unit and PM2 alternative, an Nginx server block with certbot HTTPS, the standalone output option, logs, and a two-port release script.
Read article →
Author
Anushka Dahanayake
Anushka Dahanayake builds SEO-focused websites, e-commerce platforms, dashboards, and automation systems for businesses worldwide.
