Guide

How to accept crypto on WooCommerce

There is no official halfin plugin in the WooCommerce marketplace, so this guide does the honest thing: it shows you how to wire crypto payments into WooCommerce yourself with the halfin API and hosted checkout. You register a custom payment gateway, redirect the customer to a hosted invoice at checkout, and move the WooCommerce order from pending to processing off a signed webhook — not off the browser redirect. It is a few hundred lines of a small WordPress plugin you control, and it works the same whether the customer pays in USDT, USDC, BTC, ETH, or SOL.

01

What 'accept crypto on WooCommerce' actually means here

Be clear up front about what you are building, because it changes the whole approach. halfin does not ship a packaged WooCommerce extension you install from the plugins screen and configure with an API key. What halfin provides is the underlying payment API and a hosted checkout page; accepting crypto on WooCommerce means integrating against that API the way you would integrate any custom payment gateway that redirects to an external page and reports back asynchronously.

WooCommerce is built for exactly this. Its payment-gateway system is an extension point — `WC_Payment_Gateway` is a base class you subclass — and an off-site gateway that redirects to a hosted page and then receives a server-to-server callback is the standard shape WooCommerce already supports. You are not fighting the platform; you are filling in a small custom gateway with halfin's create-invoice call and a webhook endpoint.

The result is a gateway that shows up at checkout next to your other payment methods, sends the customer to a halfin-hosted invoice to pay in the asset they hold, and flips the WooCommerce order to processing once halfin confirms the payment on-chain. The rest of this guide is the concrete steps to build it.

02

What you need before you start

Gather a few things so the integration is a wiring job, not a yak-shave. You need a halfin merchant account with a scoped API key for creating invoices, a webhook signing secret for verifying callbacks, and a WooCommerce store you can deploy a small custom plugin to. Keep both secrets in WordPress configuration (environment or `wp-config.php` constants), not hardcoded in the plugin file you commit.

Use a sandbox key while you build and test. Create invoices, pay them on a test network, and watch your webhook fire before you ever point a real customer at the flow. The full request and response schemas — exact field names, the checkout URL field, the webhook envelope — live in the API reference at docs.thehalfin.com; this guide gives you the shape, the docs give you the authoritative contract.

  • A halfin merchant account and a scoped API key for creating invoices (sandbox first).
  • A webhook signing secret, stored as a WordPress secret — never in version control.
  • A WooCommerce store and the ability to deploy a small custom plugin.
  • An endpoint URL on your store that halfin can reach to POST webhook events.
03

Step 1 — Register a custom WooCommerce payment gateway

WooCommerce discovers payment methods through the `woocommerce_payment_gateways` filter. You add a gateway class that extends `WC_Payment_Gateway`, give it an id, a title the customer sees at checkout, and a `process_payment()` method that returns a redirect. Because halfin hosts the actual payment page, your gateway does almost no UI work — it collects nothing on the checkout form itself; it just needs to hand off to a hosted invoice.

Drop this into a small plugin file (for example `wp-content/plugins/halfin-pay/halfin-pay.php`). The skeleton below registers the gateway and stubs the redirect; Step 2 fills in the create-invoice call that produces the URL you redirect to.

<?php
/* Plugin Name: halfin Pay (custom) */

add_filter( 'woocommerce_payment_gateways', function ( $gateways ) {
  $gateways[] = 'WC_Gateway_Halfin';
  return $gateways;
} );

add_action( 'plugins_loaded', function () {
  class WC_Gateway_Halfin extends WC_Payment_Gateway {
    public function __construct() {
      $this->id           = 'halfin';
      $this->method_title = 'Crypto (halfin)';
      $this->title        = 'Pay with crypto';
      $this->has_fields   = false;
      $this->init_form_fields();
      $this->init_settings();
    }

    public function process_payment( $order_id ) {
      $order = wc_get_order( $order_id );
      // Step 2 creates a halfin invoice and returns its hosted checkout URL.
      $checkout_url = halfin_create_invoice_for_order( $order );
      return array( 'result' => 'success', 'redirect' => $checkout_url );
    }
  }
} );
04

Step 2 — Create a halfin invoice for the order

When the customer places the order, your `process_payment()` creates a halfin invoice and redirects to its hosted checkout page. Anchor the invoice to fiat: send the WooCommerce order total as a string `amount_fiat` with the store's `fiat_currency` (USD, EUR, whatever your shop is priced in). halfin quotes the payable crypto amount and locks that rate when the invoice activates, so the customer pays the dollar value of the cart even if the asset's price moves while they are on the page. Do not send a crypto code as the currency and do not try to pre-pick the asset for them — the hosted page lets the customer choose the rail.

Make the create call idempotent on the order. Pass an idempotency key derived from the WooCommerce order id so that a customer who refreshes the place-order page, or a retry from your side, returns the existing invoice instead of issuing a second bill for the same cart. Carry the order id on the invoice too (in the description or a reference field per the docs) so the webhook in Step 4 can map straight back to the order without a side table.

The PHP gateway just needs the resulting hosted checkout URL to redirect to. The call itself is a plain authenticated POST — here it is as curl so the request shape is unambiguous; make the equivalent request from PHP with `wp_remote_post()`.

curl -X POST https://api.thehalfin.com/api/v1/invoices \
  -H "X-API-Key: $HALFIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "49.00",
    "fiat_currency": "USD",
    "description": "WooCommerce order #10472",
    "idempotency_key": "wc_order_10472"
  }'

# The response carries the invoice id and a hosted checkout URL on
# checkout.thehalfin.com. Redirect the customer there from process_payment().
# Store the invoice id against the order; wait for the signed webhook
# (Step 4) before marking the order paid. Full schema: docs.thehalfin.com.
05

Step 3 — Let the customer pay on hosted checkout

After the redirect, halfin owns the payment surface. The hosted checkout page on checkout.thehalfin.com renders the address, the exact payable amount, a QR code, the chosen network, and a live status as the payment confirms. The customer picks the asset they actually hold — USDT on Tron, USDC on Base or Solana, BTC, ETH, SOL, XRP, or a supported native chain asset — and pays from their wallet. You did not have to build any of that wallet UX, which is the main reason to redirect rather than render it yourself.

Set a return URL so the customer lands back on your WooCommerce order-received (thank-you) page after they pay. Treat that return strictly as navigation, not as proof of payment. The customer's browser can drop the redirect entirely — they pay on a phone wallet, the app foregrounds, the tab is gone — and the thank-you page may never load even though the money arrived. The next step is the part that actually decides whether the order is paid.

06

Step 4 — Drive order status from the signed webhook

This is the step that makes the integration reliable, so be strict about it. halfin sends your store an HMAC-signed webhook when the invoice changes state, and that event arrives server-to-server, independent of whatever the customer's browser did. Register a WordPress endpoint for it — a `register_rest_route()` handler, or a small `template_redirect` hook on a dedicated URL — and point your halfin webhook configuration at it.

Verify the signature before you trust the body. Your endpoint URL is public the moment you register it, so anyone can POST forged JSON claiming an order is paid. Recompute the HMAC over the exact raw request bytes with your signing secret, compare it to the signature header in constant time, and only then read the event. WordPress and PHP will happily hand you a parsed array — but compute the HMAC over the raw input (`file_get_contents('php://input')`) before any parsing, because a re-encoded body changes whitespace and key order and fails an otherwise-valid signature. An unsigned or mismatched request gets a 4xx and nothing else; never move an order on the payload alone.

Once verified, map the event to the order by the id you carried in Step 2, and move the WooCommerce order. On `invoice.paid`, call `$order->payment_complete()` so WooCommerce transitions the order to processing (or completed for virtual goods) and fires its own downstream hooks. Keep the handler idempotent: webhook delivery is at-least-once, so a redelivered `invoice.paid` must not complete the order twice or send a second confirmation email — `payment_complete()` on an already-paid order is a no-op, which is exactly the property you want. Acknowledge fast with a 2xx and let WooCommerce's own queues do the slow work.

  • Verify the HMAC signature over the raw body, in constant time, before acting — always.
  • Map the event to the WooCommerce order via the id you attached to the invoice.
  • On invoice.paid, call $order->payment_complete(); let WooCommerce fire its own hooks.
  • Stay idempotent — a redelivered paid event must complete the order exactly once.
  • Return 2xx fast; treat the customer's thank-you-page redirect as navigation, not proof.
07

Step 5 — Handle the awkward cases: confirming, underpaid, overpaid, expired

A crypto payment is not a single yes/no. A transaction has to land in a block and accumulate enough confirmations that a reorg is no longer a realistic risk, and customers sometimes send slightly too little or too much. WooCommerce orders should reflect those states rather than sitting in a permanent pending that your support team has to chase. halfin surfaces each as its own signed event, and the order statuses WooCommerce already has line up with them cleanly.

Map each invoice event to the order action below. The key discipline is the same as Step 4 — do not complete the order on first sight of a deposit; wait for `invoice.paid`, which means halfin has confirmed the payment past the chain's threshold with reorg-aware crediting. Leave the order in WooCommerce's on-hold or pending state while it is merely confirming, and use your existing order notes to record under/overpayment so finance can act on it.

halfin webhook eventWhat it meansWooCommerce order action
invoice.confirmingA matching deposit is seen and confirmations are accumulatingKeep the order on-hold; do not fulfil yet
invoice.paidConfirmed past the chain's threshold; settled to your balanceCall payment_complete() → order to processing/completed
invoice.underpaidA real payment arrived but is below the amount dueHold the order; add an order note with the shortfall for follow-up
invoice.overpaidSettled with a surplus over the amount dueComplete the order; note the surplus for refund or credit
invoice.expiredThe rate-lock window closed before sufficient paymentLeave the order pending/cancelled; let the customer re-checkout
08

Step 6 — Test the full path before you go live

Run the whole flow on a sandbox key before a real customer ever touches it. Place a test order, confirm your gateway redirects to a hosted invoice, pay it on a test network, and watch your webhook endpoint receive the events and the WooCommerce order move from pending to processing on its own. The integration only works if that server-to-server leg works, so prove it end to end rather than eyeballing the thank-you page.

Explicitly test the unhappy paths, because they are where a redirect-only integration silently fails. Pay an invoice and then close the browser before the redirect to confirm the order still completes from the webhook alone. Send a tampered POST to your endpoint — flip one byte of the body — and confirm your signature check rejects it with a 4xx and the order does not move. Replay a real `invoice.paid` event twice and confirm the order completes once. Let an invoice expire and confirm the customer can start a fresh checkout. When those four cases behave, swap the sandbox key for your live key and you are accepting crypto on WooCommerce.

  • Happy path: place order → redirect → pay on testnet → order auto-moves to processing.
  • Missed redirect: pay, then close the tab — the webhook must still complete the order.
  • Forged callback: a tampered POST must fail the signature check and not move the order.
  • Redelivery: the same paid event twice must complete the order exactly once.
  • Expiry: a lapsed invoice must let the customer re-checkout cleanly.