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.

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.
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()
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 useDeclare 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.txtStep 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