WooCommerce crypto payments
How to add crypto as a WooCommerce payment method: a custom gateway class, creating the Qvard payment, the webhook that completes the order, and the WooCommerce-specific pitfalls.

WooCommerce has a clean extension point for payment methods, and Qvard needs only two things from it: a call that creates a payment when the customer pays, and an endpoint that completes the order when the payment is done. There is no ready-made Qvard plugin yet, so this guide shows the custom code, piece by piece, and the WooCommerce details that usually go wrong.
Before you start
You need a Qvard account with a project, an API key and a callback URL saved in the project. On the WooCommerce side you need a way to add code, ideally a small custom plugin or a code-snippets plugin, not a theme file that disappears on the next theme update.
Two decisions up front:
- Currency. Qvard prices payments in US dollars. This guide's gateway accepts orders in USD and refuses other currencies, which is the safe default. If your shop sells in another currency, convert the order total to dollars before creating the payment and say so at checkout.
- Where the key lives. Define the API key as a constant in
wp-config.php(define( 'QVARD_API_KEY', '...' );), not in the database settings where exports and other plugins can read it.
If you only need the basic idea without the WooCommerce class, start with WordPress crypto payments.
The pieces, and how an order moves
Three pieces make the integration:
- A payment gateway class that appears in WooCommerce checkout as "Pay with crypto".
- A call to Qvard inside the gateway, which creates a payment for the order and returns the address of the hosted payment page.
- A webhook endpoint that Qvard calls when the payment is completed.
An order then moves like this: the customer chooses the method and presses pay; the gateway creates the Qvard payment, puts the order on hold and redirects the customer to the payment page; the customer pays from a wallet; Qvard calls your endpoint; the endpoint completes the order and WooCommerce sends its usual emails and moves it on. The payment id you send to Qvard is the WooCommerce order number, which is how the webhook finds the order again.
The gateway class
The class extends WC_Payment_Gateway. Its constructor sets the method id and title and loads the settings, and process_payment does the work. It refuses non-USD orders, then sends one POST to https://api.qvard.com/api/v1/payment with your key in the Authorization: Bearer header and a JSON body: id (the order number as a string) and price (the total in US dollars). A 201 answer carries a redirectUrl.
On success the gateway stores the Qvard payment id on the order, sets the status to on-hold ("awaiting payment"), empties the cart and returns the redirectUrl as the redirect. On failure it adds an error notice and returns failure, so the customer stays on the checkout page and can choose another method.
Sending the same id for the same payment method returns the existing payment, so a customer who presses the button twice does not create two payments.
<?php
// A minimal WooCommerce payment method that sends the customer to the Qvard payment page.
add_filter( 'woocommerce_payment_gateways', function ( $gateways ) {
$gateways[] = 'WC_Gateway_Qvard';
return $gateways;
} );
add_action( 'plugins_loaded', function () {
if ( ! class_exists( 'WC_Payment_Gateway' ) ) {
return;
}
class WC_Gateway_Qvard extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'qvard';
$this->method_title = 'Qvard (crypto)';
$this->method_description = 'Accept USDT and other crypto through Qvard.';
$this->has_fields = false;
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title', 'Pay with crypto' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
}
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array( 'title' => 'Enable', 'type' => 'checkbox', 'default' => 'no' ),
'title' => array( 'title' => 'Title', 'type' => 'text', 'default' => 'Pay with crypto' ),
);
}
public function process_payment( $order_id ) {
$order = wc_get_order( $order_id );
// Qvard prices payments in US dollars: refuse other shop currencies instead of charging a wrong amount.
if ( 'USD' !== $order->get_currency() ) {
wc_add_notice( 'Crypto payment is available for orders in USD only.', 'error' );
return array( 'result' => 'failure' );
}
$response = wp_remote_post( 'https://api.qvard.com/api/v1/payment', array(
'timeout' => 15,
'headers' => array( 'Authorization' => 'Bearer ' . QVARD_API_KEY, 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array( 'id' => (string) $order->get_id(), 'price' => (float) $order->get_total() ) ),
) );
if ( is_wp_error( $response ) || 201 !== wp_remote_retrieve_response_code( $response ) ) {
wc_add_notice( 'Could not start the payment. Please try again.', 'error' );
return array( 'result' => 'failure' );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $data['redirectUrl'] ) ) {
wc_add_notice( 'Could not start the payment. Please try again.', 'error' );
return array( 'result' => 'failure' );
}
$order->update_meta_data( '_qvard_payment_id', sanitize_text_field( $data['id'] ?? '' ) );
$order->update_status( 'on-hold', 'Waiting for the crypto payment.' );
WC()->cart->empty_cart();
return array( 'result' => 'success', 'redirect' => $data['redirectUrl'] );
}
}
} );The webhook that completes the order
Register a REST route and save its address in your Qvard project as the callback URL. When a payment is completed, Qvard sends a POST with the id and the status, signed in the X-Qvard-Signature header. The handler in the example:
- verifies the signature (HMAC-SHA256 with your secret key) with the Qvard SDK and answers
401if it does not match; - finds the order by
idand ignores orders that do not exist or were not paid with this gateway, still answering 200 so Qvard stops retrying; - calls
payment_complete()only on thecompletedstatus and only if the order is not paid yet, which makes it safe to run twice; - answers exactly HTTP 200. Any other status, a timeout or a redirect counts as a failed delivery and is retried, five attempts over about six minutes.
Allow the route in any security plugin or firewall, and make sure caching does not touch /wp-json/. The wider rules are in how to secure crypto payment webhooks.
<?php
// Callback URL to save in the Qvard project: https://your-shop.com/wp-json/qvard/v1/webhook
add_action( 'rest_api_init', function () {
register_rest_route( 'qvard/v1', '/webhook', array(
'methods' => 'POST',
'permission_callback' => '__return_true', // authenticated below by the X-Qvard-Signature header
'callback' => function ( WP_REST_Request $request ) {
try {
$payload = ( new Qvard\Qvard( QVARD_API_KEY ) )->webhooks->constructEvent( $request->get_body(), $request->get_header( 'x-qvard-signature' ), QVARD_API_KEY );
} catch ( \Exception $e ) {
return new WP_REST_Response( null, 401 );
}
$order = wc_get_order( (int) ( $payload['id'] ?? 0 ) );
// Unknown order or not ours: nothing to retry, answer 200.
if ( ! $order || 'qvard' !== $order->get_payment_method() ) {
return new WP_REST_Response( null, 200 );
}
if ( 'completed' === ( $payload['status'] ?? '' ) && ! $order->is_paid() ) {
$order->payment_complete( (string) $order->get_meta( '_qvard_payment_id' ) );
$order->add_order_note( 'Qvard: payment completed.' );
}
return new WP_REST_Response( null, 200 ); // must be exactly 200
},
) );
} );WooCommerce details that bite
- Stock. WooCommerce reduces stock when an order goes to processing or on hold, so unpaid crypto orders can hold stock. Cancel stale on-hold orders after a period you choose, with a scheduled job of your own (WooCommerce's hold-stock setting applies to pending orders, not to on-hold ones), otherwise stock stays reserved.
- Partial payments. If less than the full amount arrives the payment status is
partialand no completion webhook is sent. The order stays on hold; add a note, ask the customer for the rest or cancel and refund. - Refunds. A crypto payment cannot be pulled back. Create the refund record in WooCommerce for your books, but send the money yourself to an address the customer gives you.
- Order emails. The "on hold" email goes out when the order is placed; make sure its text tells the customer that payment is pending and where to pay.
- The return trip. Return URLs after payment are not available yet, so the customer comes back to your shop on their own. The standard order page shows the real status, which is right.
- Block checkout. WooCommerce's block-based checkout needs a gateway registered for blocks as well; test the method in the checkout you actually use.
Test before you go live
- Open a demo invoice to see the payment page your customer will get.
- Place a test order with the crypto method and check that you are redirected to the payment page and that the order is on hold.
- Pay a small real amount and confirm that the webhook arrives and the order becomes processing.
- Make the endpoint fail once, confirm that Qvard retries and that the order is not completed twice.
- Withdraw a small amount from your balance to your wallet to know the whole path works.
There is no separate sandbox yet, so use small real amounts.
What it costs and what is ready
There is no subscription fee for the gateway. The commission is individual to your project and, per the pricing page, starts at 1% and can go down to 0.1%. Funds sit on your Qvard balance and you withdraw them to your own wallet for the network fee; withdrawal to a bank account in fiat is not available.
Qvard works through its API, so the integration takes a developer and some testing; there is no ready-made WooCommerce plugin, and payment links and a separate sandbox are in development. The API itself is described in crypto payment API for developers, and the store side in crypto payments for ecommerce.
FAQ
Is there a Qvard plugin for WooCommerce?
Not yet. The integration is through the API: a gateway class that creates the payment and a webhook that completes the order. The code in this guide is a starting point for a developer.
My shop uses euros. Can customers still pay in crypto?
Qvard prices payments in US dollars. Convert the order total to dollars before creating the payment, and tell the customer the dollar amount at checkout; the example gateway refuses non-USD orders as a safe default.
When does the order become paid?
When the webhook arrives with the completed status. A customer returning to the shop is not proof of payment, and the partial status means less than required arrived.
What if my site was down when the payment completed?
The notification is retried five times over about six minutes. After that, resend it for the payment from the Qvard cabinet; the handler is safe to run twice.
