When a client asked me to let Indian customers pay directly from their bank accounts, I thought “just enable the built-in Bank Transfer gateway”. A few minutes later the order status stayed at Pending payment even after the bank confirmed the debit. The missing piece turned out to be a webhook that the bank’s API fires once the transaction is settled. Below is the exact flow I ended up using, plus the pitfalls you’ll hit if you skip any step.
Why Pay by Bank matters for WooCommerce
Unlike credit cards, a bank-to-bank transfer does not require a PCI-compliant form. The user is redirected to their online banking portal, authenticates, and authorises the payment. For merchants the advantage is lower transaction fees and higher conversion in markets where cards are unpopular. WooCommerce ships with a simple “Direct Bank Transfer” method, but that only records a manual instruction, it never talks to the bank. To get real-time confirmation you need a proper gateway plugin that talks to the bank’s API and listens for a webhook.
Choosing a gateway plugin
I evaluated three options that claim “Pay by Bank” support:
- Plugin A, free, but only supports a single bank and has no sandbox.
- Plugin B, paid, supports many Indian banks, offers sandbox, and lets you map webhook URLs.
- Custom integration, build your own using the bank’s REST API.
For an evergreen guide I’ll assume you use Plugin B because it balances cost, documentation, and sandbox availability. The steps are the same for any similar plugin; you just replace the UI labels.
Step 1, Install and activate the plugin
- Upload the zip via Plugins → Add New → Upload Plugin.
- Activate it.
- A new submenu appears under WooCommerce → Settings → Payments called Pay by Bank.
Step 2, Configure credentials and sandbox mode
In the Pay by Bank settings page you’ll see fields for:
- API Key, issued from the bank’s developer portal.
- API Secret, keep this secret; it’s used to sign requests.
- Environment, choose Sandbox for testing, Production for live.
- Redirect URL, WooCommerce automatically fills this, but you can override it if you run a custom checkout page.
Enter the sandbox credentials you generated on the bank’s portal. Save changes. The plugin now points to the bank’s test endpoint, which mimics real transactions without moving money.
Step 3, Create a test product and place an order
Create a simple product priced at ₹100. Add it to the cart, go to checkout, and select Pay by Bank. You’ll be redirected to the bank’s sandbox login page. Use the test user testuser and password password123. After you “authorize” the payment the bank redirects you back to WooCommerce with a query string like:
?order_id=1234&status=success&transaction_id=ABCD1234
WooCommerce receives this and marks the order Processing, but only if the webhook fires. In the sandbox the bank sends a POST request to the webhook URL you’ll set next.
Step 4, The often-missed webhook setting
When I first looked at the plugin UI, the webhook field was hidden under an Advanced tab labeled “Payment Confirmation URL (optional)”. I left it blank, assuming the redirect was enough. The bank still posted a payload like:
{
"order_id": "1234",
"transaction_id": "ABCD1234",
"status": "settled",
"amount": 10000,
"currency": "INR"
}
Because WooCommerce never saw that POST, the order stayed Pending payment.
How to set the webhook correctly
- In the plugin’s Advanced tab copy the Webhook URL that looks like:
https://yourdomain.com/wp-json/paybybank/v1/notify - Log into the bank’s developer portal → Webhooks → Create New.
- Paste the URL, select Payment Settled event, and enable HMAC verification.
- Save. The bank will now sign each payload with a SHA-256 hash using your API Secret.
Step 5, Verify webhook signatures in WordPress
The plugin already verifies the signature, but if you ever write a custom handler you’ll need something like this:
<?php
add_action('rest_api_init', function () {
register_rest_route('paybybank/v1', '/notify', [
'methods' => 'POST',
'callback' => 'handle_paybybank_webhook',
'permission_callback' => '__return_true',
]);
});
function handle_paybybank_webhook( WP_REST_Request $request ) {
$payload = $request->get_body();
$signature = $request->get_header('X-PBB-Signature');
$secret = defined('PBB_API_SECRET') ? PBB_API_SECRET : '';
// Compute HMAC
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
return new WP_Error('invalid_signature', 'Signature mismatch', ['status' => 401]);
}
$data = json_decode($payload, true);
$order_id = $data['order_id'];
$order = wc_get_order($order_id);
if ($order && $data['status'] === 'settled') {
$order->payment_complete($data['transaction_id']);
}
return new WP_REST_Response('ok', 200);
}
?>
register_rest_routecreates the endpoint.hash_hmacrecreates the signature using the secret.payment_completeupdates the order status to Completed.
If you use the official plugin you can skip the code, but keeping it handy helps when the provider changes the header name.
Step 6, Switch to production
Before you flip the Environment toggle, do a final sanity check:
- Verify the live API Key and Secret are stored in
wp-config.php(never in the database). - Test a real-world small amount (₹1) to confirm the redirect and webhook work.
- Ensure your SSL certificate is valid, the bank rejects non-HTTPS URLs.
Once you’re satisfied, change the environment to Production and update the webhook URL on the bank’s portal to the same endpoint (the URL itself does not change, only the domain’s certificate). Orders will now move straight to Processing as soon as the bank settles the transfer.
Common pitfalls and how to avoid them
- Forgot the webhook, the order will never move past Pending.
- Mismatched currency, the plugin validates INR only; using a different store currency will cause the API to reject the request.
- SSL misconfiguration, self-signed certificates work in sandbox but are rejected in production.
- Hard-coded API secret, storing it in
wp-config.phpwithdefine('PBB_API_SECRET', '…');keeps it out of the admin UI and away from accidental exposure. - Testing with a cached page, clear WooCommerce transients after each sandbox run; otherwise the plugin may think the webhook was already processed.
Debugging tips
- Log incoming webhook, add
error_log('PBB webhook: ' . $request->get_body());at the top of the handler. - Check response codes, the bank’s portal shows a delivery status; a 401 means signature mismatch, a 200 means success.
- Use a request inspector, services like https://webhook.site let you see the exact payload before it reaches your server.
FAQ
How do I test the webhook without a real bank transaction?
Create a temporary endpoint on https://webhook.site, copy the URL into the bank’s sandbox webhook field, and trigger a Test Notification from the portal. Then replace the URL with your live endpoint.
What if the order stays pending even after the webhook fires?
Check the order ID in the payload matches the WooCommerce order ID. Also verify the signature header name (X-PBB-Signature in my example) matches what the bank sends.
Can I use Pay by Bank for subscriptions?
Yes, but the gateway must support recurring token creation. The plugin I used only handles one-off payments, so for subscriptions you’ll need a gateway that exposes a Create Mandate API.
Wrapping up
If you follow the steps above, Pay by Bank becomes a frictionless checkout option for your Indian audience, no cards, no extra fees, and instant order confirmation thanks to the webhook. The biggest surprise for me was how easy it is to miss the webhook setting; once you add it, the whole flow works like a charm. Feel free to drop a comment if you hit a snag, I’ll help you troubleshoot.