In Turkey, a site taking online food orders that cannot accept meal cards loses a large share of its audience at the payment step. WooCommerce does not support these cards out of the box; the answer is a custom payment gateway.
To accept Multinet, Sodexo or Setcard in WooCommerce there is no off-the-shelf plugin; you write a custom gateway extending WC_Payment_Gateway against the provider's integration documentation. The critical point is never trusting the parameters on the URL the customer returns to: the payment must be confirmed by a separate server-to-server query on return. Only after that confirmation should the order be marked paid.
WooCommerce's payment ecosystem is built largely around international providers and locally common virtual POS providers. Meal-card providers sit in a different category: contract-bound, requiring merchant approval, and sharing integration documentation only after an agreement.
So you will not find an install-and-go solution in the plugin directory. Be sceptical of plugins claiming otherwise, too: the payment flow depends on the provider's own API version and on merchant parameters specific to you.
The first step is commercial, not technical: sign the merchant agreement, obtain test credentials and the integration documentation. Code written without them is guesswork.
A WooCommerce payment method is a class extending WC_Payment_Gateway, registered through the woocommerce_payment_gateways filter.
add_filter( 'woocommerce_payment_gateways', function ( $gateways ) {
$gateways[] = 'WC_Gateway_Meal_Card';
return $gateways;
} );
class WC_Gateway_Meal_Card extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'meal_card';
$this->method_title = 'Meal Card';
$this->method_description = 'Pay with a meal card.';
$this->has_fields = false;
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->enabled = $this->get_option( 'enabled' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id,
array( $this, 'process_admin_options' ) );
}
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
// Start the transaction with the provider and get a redirect URL.
$redirect = $this->start_transaction( $order );
if ( is_wp_error( $redirect ) ) {
wc_add_notice( $redirect->get_error_message(), 'error' );
return array( 'result' => 'failure' );
}
return array(
'result' => 'success',
'redirect' => $redirect,
);
}
}
Note what this does not do: process_payment does not mark the order paid. It only sends the customer to the provider's verification screen. Whether payment happened is unknown at this stage.
Meal-card integrations are typically redirect-based:
Step six is what this note is about.
The most frequent error I see: the provider returns the customer to something like /payment-return/?status=success&order=1234, and the code reads status=success and treats the order as paid.
That URL passes through the customer's browser. Anyone who types the parameter by hand can mark an order paid without paying. In restaurant orders the consequence is direct loss of goods — the order reaches the kitchen, gets prepared and gets delivered.
The correct behaviour: the return step only tells you the customer came back. The real decision is asked of the provider.
public function handle_return() {
$order_id = absint( $_GET['order_id'] ?? 0 );
$order = wc_get_order( $order_id );
if ( ! $order || $order->is_paid() ) {
return; // missing, or already paid — do not process twice
}
// The decision comes from the provider, not the browser:
$status = $this->query_transaction_status( $order->get_id() );
if ( is_wp_error( $status ) || 'approved' !== $status['state'] ) {
$order->update_status( 'failed', 'Meal card payment was not approved.' );
wp_safe_redirect( wc_get_checkout_url() );
exit;
}
// Verify the amount too — never accept an approval for less.
if ( ! $this->amounts_match( $status['amount'], $order->get_total() ) ) {
$order->update_status( 'on-hold', 'Amount mismatch, needs manual review.' );
return;
}
$order->payment_complete( $status['transaction_id'] );
wp_safe_redirect( $this->get_return_url( $order ) );
exit;
}
Three protections at once: the state is queried from the provider, the amount is compared, and an already-paid order is not reprocessed. That last one matters — refreshing the return page or a repeated provider callback must not complete the order twice.
Using WooCommerce order states correctly is half the integration, for accounting and operations alike:
| State | When | Why |
|---|---|---|
pending | Order created, payment started | No money yet; must not reach the kitchen |
failed | Provider declined or customer cancelled | Customer can retry |
on-hold | Ambiguity such as an amount mismatch | No automatic decision; a human must look |
processing | Payment confirmed | payment_complete() sets this itself |
A common mistake is marking payment with update_status('processing') by hand. Use payment_complete() instead: it records the transaction id, reduces stock, fires the relevant hooks and picks the correct state for the store type.
WooCommerce's block-based checkout works differently from the classic shortcode page: payment methods are listed on the React side. A gateway written the classic way may not appear at all in block checkout.
add_action( 'woocommerce_blocks_loaded', function () {
if ( ! class_exists( 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' ) ) {
return;
}
require_once __DIR__ . '/class-meal-card-blocks.php';
add_action(
'woocommerce_blocks_payment_method_type_registration',
function ( $registry ) {
$registry->register( new WC_Meal_Card_Blocks_Support() );
}
);
} );
Because the block side registers via JavaScript, the gateway needs a small JS file too. That file must not be deferred or concatenated — otherwise the payment method never appears in the list. See the empty cart note for the details of that trap.
failed, and the cart must be preserved.wp-config.php constants, never in version control.A meal-card integration is a standard application of writing a WooCommerce payment gateway; the difficulty lies in reading the provider's documentation correctly and putting the trust boundary in the right place. Nothing arriving from the customer's browser is proof of payment. An order should be marked paid only after a server-to-server confirmed approval.
This approach runs live in the Her Mutfak ordering system.
In practice, no. Meal-card providers share integration details only with businesses that hold a merchant agreement, and the parameters are business-specific. The solution is a custom payment gateway extending WC_Payment_Gateway, built against the provider's documentation.
Because that URL passes through the customer's browser and can be edited. A system that only reads 'success' from the URL allows orders to be created without any payment. The correct method is a separate server-to-server query on return, verifying both the transaction state and the amount.
Block-based checkout lists payment methods on the JavaScript side. A gateway written for classic checkout must also register block support. Additionally, if the gateway's block JS file is deferred or concatenated by an optimisation plugin, the method never appears in the list.
payment_complete() does more than change status: it records the transaction id, reduces stock, fires the relevant hooks and selects the correct order state for the store type. Setting the status manually skips those steps and leaves silent errors in reporting and stock.