Saturday, September 12, 2026

Custom Payment Gateway Integration in WooCommerce: A Complete Developer's Guide

Your client's regional acquirer has no WooCommerce plugin. The merchant is losing sales every day the checkout doesn't work. The processor's API documentation is dense, the existing plugins on the market are abandoned or bloated, and the deadline is real. This is the situation where custom payment gateway integration in WooCommerce stops being a theoretical exercise and becomes the only path forward.

WooCommerce runs on millions of live sites, yet its built-in payment options cover only a fraction of the processors merchants actually want to use. When a business needs a regional acquirer, a niche BNPL provider, or a proprietary internal payment system, e-commerce developers must build the bridge themselves. This guide walks professional developers through the architecture of WooCommerce payments, a working gateway plugin with verified webhooks and refund support, and the security and compliance requirements that separate a prototype from a production-ready integration.

WooCommerce custom payment gateway decision
Source: woocommerce.com

Does Your WooCommerce Project Actually Need a Custom Payment Gateway?


Before writing a single line of code, confirm that building from scratch is the right call. A custom payment gateway is justified when:

1. No official plugin exists for your target processor or market.
2. Existing plugins are bloated, unmaintained, or incompatible with your stack.
3. The merchant requires a bespoke checkout experience, such as split payments or internal credit ledgers.
4. Compliance demands full control over where card data flows.

If a well-maintained official plugin exists, use it. Build custom only when the commercial or technical requirement genuinely cannot be met otherwise. The rest of this guide assumes you've made that call and are committed to shipping your WooCommerce payment integration.

WooCommerce payment architecture diagram
Source: help.aura-software.com

Understanding the WooCommerce Payment Architecture


WooCommerce does not process payments itself. It orchestrates them. The platform provides a standardized contract that every gateway must fulfill, then delegates the actual transaction to an external processor through an API call, a redirect, or an embedded form.

Here's the full lifecycle at a glance:

Customer → WooCommerce Checkout → process_payment() → Processor API

Redirect / Hosted Page

Customer completes payment → Processor → Webhook → WooCommerce

Order status updated → payment_complete()


WC_Payment_Gateway class PHP code
Source: github.com

The WC_Payment_Gateway Class


Every gateway extends the abstract WC_Payment_Gateway class. This parent class supplies settings management, admin UI rendering, and the method registry that makes your gateway appear in WooCommerce → Settings → Payments.

Key properties you will define include:

- $id — a unique slug such as acme_gateway
- $method_title and $method_description — labels for the admin interface
- $has_fields — whether the gateway renders custom checkout fields
- $supports — an array declaring features. The full set you'll commonly use:
- products — one-time purchases
- refunds — partial and full refunds from the admin
- subscriptions — recurring billing via WooCommerce Subscriptions
- add_payment_method — saving a card from the My Account page
- tokenization — storing payment methods for later use

Declare only what you actually implement. Claiming refunds without a working process_refund() produces broken admin buttons and support tickets.

Checkout Flow and Hooks


The transactional lifecycle moves through process_payment(), an optional redirect to the processor, and a return or webhook that finalizes the order. WooCommerce fires hooks at each stage — woocommerce_checkout_order_processed, woocommerce_payment_complete, and woocommerce_order_status_failed — giving you clean insertion points for logging, notifications, and reconciliation.

Step-by-Step Custom Payment Gateway Integration Walkthrough


Now that you've decided to build, here's how the pieces fit together. Each step builds on the previous one; read them in sequence the first time through.

Step 1: Scaffold a Dedicated Plugin for Your Gateway


Never place gateway code in a theme's functions.php. Create a standalone plugin with a clear directory structure, a PSR-4 autoloader, and a Requires Plugins: woocommerce header. This isolation prevents fatal errors when WooCommerce is deactivated.

acme-gateway/
├── acme-gateway.php
├── includes/
│ ├── class-wc-gateway-acme.php
│ └── class-acme-webhook-handler.php
├── composer.json
└── readme.txt


Step 2: Extend WC_Payment_Gateway


Initialize the gateway inside the plugins_loaded hook, after verifying WooCommerce is active:

add_filter( 'woocommerce_payment_gateways', 'acme_register_gateway' );
function acme_register_gateway( $gateways ) {
$gateways<a href="https://woocommerce.com/community-slack/">] = 'WC_Gateway_Acme';
return $gateways;
}


Your class constructor populates init_form_fields() with API keys, sandbox toggles, and title settings, then calls init_settings(). Persist admin changes by hooking woocommerce_update_options_payment_gateways_{$this->id} to process_admin_options().

Step 3: Handle process_payment()


This is the method that does the real work, and it's where most integrations live or die. It returns an array with a result and a redirect key. A minimal hosted-redirect implementation looks like this:

public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );

$response = wp_remote_post( $this->api_endpoint . '/charges', [
'headers' => [
'Authorization' => 'Bearer ' . $this->secret_key,
'Idempotency-Key' => $order->get_order_key(), // prevents duplicate charges
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'amount' => (int) round( $order->get_total() <em> 100 ),
'currency' => $order->get_currency(),
'reference' => $order->get_id(),
] ),
'timeout' => 30,
] );

if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
wc_add_notice( __( 'Payment could not be initiated.', 'acme-gateway' ), 'error' );
return [ 'result' => 'failure' ];
}

$body = json_decode( wp_remote_retrieve_body( $response ), true );
$order->update_meta_data( '_acme_transaction_id', sanitize_text_field( $body['id'] ) );
$order->save();

return [
'result' => 'success',
'redirect' => esc_url_raw( $body['checkout_url'] ),
];
}


Note the idempotency key. It is not optional. Networks retry; without it, a single customer click can become two charges.

Step 4: Process Webhooks Securely


Asynchronous notifications are the source of truth for final order status. Register a REST route or an admin-post.php endpoint, and always verify the signature. Two details trip up most developers:

1. Read the raw request body, not $_POST. JSON webhooks won't populate $_POST at all.
2. Use hash_equals(), not ===, for the signature comparison. String comparison is timing-attack vulnerable.

public function verify_webhook( WP_REST_Request $request ) {
$raw_body = $request->get_body();
$signature = $request->get_header( 'x-acme-signature' );
$expected = hash_hmac( 'sha256', $raw_body, $this->webhook_secret );

if ( ! hash_equals( $expected, (string) $signature ) ) {
return new WP_Error( 'invalid_signature', 'Signature mismatch', [ 'status' => 401 ] );
}
return true;
}


Reject unsigned payloads without exception. Log the rejection, but never act on it.

Step 5: Manage Order Status and Refunds


If your gateway declares refunds in $supports, implement process_refund( $order_id, $amount, $reason ). Return true on success, or a WP_Error with a human-readable message:

public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
$txn = $order->get_meta( '_acme_transaction_id' );

$response = wp_remote_post( $this->api_endpoint . "/charges/{$txn}/refunds", [
'headers' => [ 'Authorization' => 'Bearer ' . $this->secret_key ],
'body' => [ 'amount' => (int) round( $amount </em> 100 ) ],
] );

if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'refund_failed', __( 'The processor rejected the refund.', 'acme-gateway' ) );
}
return true;
}


Accurate status transitions — pending to processing or failed — keep inventory and reporting trustworthy. When you get them wrong, downstream systems (fulfillment, accounting, email) all drift.

Step 6: Handle Failure Recovery


A webhook will eventually be missed. The processor's server will hiccup, your endpoint will time out, or a firewall rule will silently drop the request. Production integrations need a reconciliation path:

- Store the processor's transaction ID against the order (as shown in Step 3).
- Schedule a daily cron job that queries the processor for orders stuck in pending beyond a threshold (e.g., 24 hours) and syncs their status.
- Expose a manual "Sync with processor" action in the order admin screen so support staff can resolve edge cases without developer intervention.

This is the part that separates gateways that survive Black Friday from gateways that generate angry support tickets in January.

Security and Compliance Essentials for WooCommerce Payments


PCI DSS Scope


Your compliance burden depends entirely on how card data reaches your server. Hosted payment pages and tokenized fields keep you in SAQ A, the lightest validation tier. Directly posting card numbers to your own endpoint pushes you toward SAQ D, which requires network segmentation, quarterly scans, and substantially more documentation. Choose the hosted path unless you have a compelling reason not to.

Idempotency and Replay Protection


Networks retry. Always send a unique idempotency key with each authorization request and store the processor's transaction reference against the order. This prevents duplicate charges when a webhook or redirect fires twice — a leading cause of support tickets in custom integrations.

Logging and Debugging


Use WC_Logger rather than error_log(). Log request payloads with sensitive fields redacted, and gate verbose logging behind a sandbox toggle so production logs stay lean.

$logger = wc_get_logger();
$logger->info( 'Charge initiated', [
'source' => 'acme-gateway',
'context' => [ 'order_id' => $order_id, 'amount' => $amount ],
] );


Never log full card numbers, CVVs, or API secrets — even in sandbox mode.

Testing Your Custom Gateway Integration


A gateway is not finished until it survives failure. Test the following scenarios before launch:

- Successful authorization, capture, and settlement
- Declined cards and insufficient-funds responses
- Timeout and network failure between WooCommerce and the processor
- Duplicate webhook delivery
- Partial and full refunds
- Currency mismatch and zero-decimal currency handling

Methodology matters as much as the scenario list. Use the processor's sandbox environment for all functional tests. For webhook testing on a local machine, tunnel your dev site with a tool like ngrok or Expose so the processor can reach your endpoint. Build a small mock processor that returns canned responses for each failure mode — this lets you test timeout handling and malformed payloads without waiting on the real API.

WooCommerce's built-in HPOS (High-Performance Order Storage) compatibility must also be declared via FeaturesUtil::declare_compatibility(), since direct postmeta queries will break under the new order tables.

Best Practices Checklist


Beyond the fundamentals already covered, these practices separate production-grade gateways from working prototypes:

| Practice | Why It Matters |
|---|---|
| Verify nonces on all admin forms | Prevents CSRF attacks on settings pages |
| Check current_user_can( 'manage_woocommerce' ) before privileged actions | Blocks unauthorized refund or status changes |
| Sanitize and escape every input and output | Prevents XSS in admin-rendered transaction data |
| Use the Settings API for credentials | Keeps secrets out of code and version control |
| Verify webhook signatures with hash_equals() | Blocks spoofed confirmations and timing attacks |
| Declare HPOS compatibility | Future-proofs against core upgrades |
| Translate all user-facing strings | Supports international storefronts |
| Never trust client-side totals | Prevents price manipulation attacks |
| Handle zero-decimal currencies (JPY, KRW) | Avoids 100x overcharges |
| Send idempotency keys on every mutation | Prevents duplicate charges on retry |

Frequently Asked Questions


Can I integrate a gateway without writing PHP?
Only if the processor publishes an official WooCommerce plugin. Otherwise, custom integration requires PHP development.

Does a custom gateway work with WooCommerce Subscriptions?
Yes, provided you declare subscriptions in $supports and implement the recurring payment hooks. Subscriptions adds meaningful complexity — budget accordingly.

Why is my webhook firing twice?
Almost always because the processor retried after your endpoint returned a non-2xx response, or because your handler isn't idempotent. Store the event ID on first receipt and short-circuit duplicates. Also confirm you're returning HTTP 200 before doing slow work like sending emails.

How do I handle 3DS / SCA?
Hosted payment pages handle this transparently — the customer is redirected to the processor, who manages the challenge. If you're doing direct API integration, you'll need to implement the redirect-and-return flow yourself and handle the authentication_required response code. For most merchants, the hosted path is dramatically simpler.

How long does integration typically take?
A hosted-redirect gateway takes roughly 40–80 developer hours. Direct API integration with tokenization, refunds, and 3DS commonly exceeds 150 hours.

Conclusion


Custom payment gateway integration in WooCommerce is a structured, repeatable engineering task. Extend WC_Payment_Gateway, route the transaction through process_payment(), verify every webhook with hash_equals(), and treat idempotency and failure recovery as first-class requirements rather than afterthoughts. Get those fundamentals right and you unlock any processor your clients demand — regional acquirers, emerging BNPL networks, or fully proprietary payment rails.

Your next concrete steps:

1. Confirm the decision-to-build criteria in the first section actually apply.
2. Scaffold the plugin and register a gateway that appears in WooCommerce settings.
3. Ship a working sandbox transaction end-to-end before adding refunds or subscriptions.
4. Add webhook verification and a reconciliation cron before going live.

When you get stuck — and you will — the [WooCommerce Community Slack and the woocommerce GitHub repository are where the developers who've hit the same wall tend to gather.

Further Reading


- WooCommerce Payment Gateway API documentation (opens in a new window)
- PCI Security Standards Council — SAQ validation documents (opens in a new window)
- WooCommerce High-Performance Order Storage guide (opens in a new window)
- WooCommerce Subscriptions — payment gateway integration (opens in a new window)

---

Enjoyed this guide? Subscribe Now to get our next deep-dive on WooCommerce payment architecture delivered straight to your inbox — no fluff, just field-tested engineering.custom payment gateway, e-commerce developers, integration, woocommerce, security

0 comments:

Post a Comment