A payment method
A payment method in WooCommerce is called a payment gateway. It is a class extending WC_Payment_Gateway: it describes its settings and one main method, process_payment(), which decides what to do with the order after Place order is pressed.
A simple gateway
Let us start with payment by invoice — without calling a bank. Create the file includes/class-acme-gateway.php:
// wp-content/plugins/acme-shop/includes/class-acme-gateway.php
class Acme_Gateway extends WC_Payment_Gateway
{
public function __construct()
{
$this->id = 'acme_invoice';
$this->method_title = 'Pay by invoice';
$this->method_description = 'The customer receives an invoice and pays by transfer.';
$this->has_fields = false;
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields(): void
{
$this->form_fields = [
'enabled' => ['title' => 'Enable', 'type' => 'checkbox', 'default' => 'no'],
'title' => ['title' => 'Title', 'type' => 'text', 'default' => 'Pay by invoice'],
'description' => ['title' => 'Description', 'type' => 'textarea', 'default' => 'The invoice will be emailed.'],
];
}
public function process_payment($orderId): array
{
$order = wc_get_order($orderId);
$order->update_status('on-hold', 'Awaiting payment by invoice.');
WC()->cart->empty_cart();
return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
}The constructor sets the ID and reads the settings. The form_fields array becomes the gateway's settings page, and the add_action line saves it. process_payment() puts the order on hold, empties the cart and returns the address of the Order received page.
Registration
// wp-content/plugins/acme-shop/acme-shop.php
add_filter('woocommerce_payment_gateways', function (array $gateways): array {
require_once __DIR__ . '/includes/class-acme-gateway.php';
$gateways[] = Acme_Gateway::class;
return $gateways;
});Open WooCommerce → Settings → Payments: Pay by invoice has appeared in the list. Enable it — the method becomes available on the classic checkout page.
What process_payment returns
| Key | Value |
|---|---|
result | success or failure |
redirect | Where to send the customer on success |
On failure call wc_add_notice('Error text', 'error') and return ['result' => 'failure'] — the customer stays on the checkout page.
Paying through a bank
A real gateway creates a payment at the payment service and sends the customer to its page. Add the field 'api_key' => ['title' => 'API key', 'type' => 'password'] to form_fields and replace the method:
// wp-content/plugins/acme-shop/includes/class-acme-gateway.php
public function process_payment($orderId): array
{
$order = wc_get_order($orderId);
$response = wp_remote_post('https://pay.example.com/v1/payments', [
'headers' => ['Authorization' => 'Bearer ' . $this->get_option('api_key')],
'body' => [
'amount' => $order->get_total(),
'currency' => $order->get_currency(),
'order' => $order->get_id(),
'return_url' => $this->get_return_url($order),
],
]);
if (is_wp_error($response)) {
wc_add_notice('Could not reach the bank. Please try again.', 'error');
return ['result' => 'failure'];
}
$payment = json_decode(wp_remote_retrieve_body($response), true);
$order->update_meta_data('_acme_payment_id', $payment['id']);
$order->save();
return ['result' => 'success', 'redirect' => $payment['checkout_url']];
}The order stays in Pending payment, the payment ID is saved in it, and the customer goes to the bank page. Every service has its own address and request fields — here they are made up.
The bank's notification
The bank reports a payment with a separate request to the site. WooCommerce has a ready-made address for it, /wc-api/acme_invoice/:
// wp-content/plugins/acme-shop/includes/orders.php
add_action('woocommerce_api_acme_invoice', function () {
$payload = json_decode(file_get_contents('php://input'), true);
$orders = wc_get_orders([
'limit' => 1,
'meta_query' => [['key' => '_acme_payment_id', 'value' => $payload['id'] ?? '']],
]);
if ($orders && ($payload['status'] ?? '') === 'paid') {
$orders[0]->payment_complete($payload['id']);
}
status_header(200);
exit;
});payment_complete() moves the order to Processing, reduces stock and triggers emails. Before that the request signature must be verified the way the bank's documentation describes — otherwise anyone who knows the address can pay for an order.
The block checkout
In the block checkout the gateway does not appear by itself: it has to be registered on the JavaScript side as well. Until that is done, use the classic page with the [woocommerce_checkout] shortcode.
Common mistake
An order is marked as paid in process_payment() or on the return page — before the bank's notification. The customer closed the tab or forged the return address, and the goods leave without money. Only a verified notification makes an order paid. And card data must not pass through your server: it is entered on the payment service's page.