Guide

How to accept crypto on Magento

There is no official halfin Magento extension on the Marketplace, so this guide does the honest thing: it shows you how to wire crypto payments into Magento (Adobe Commerce) yourself with the halfin API and hosted checkout. You add a small custom payment method, redirect the customer to a hosted invoice at the order step, and move the Magento order from pending to processing off a signed webhook — not off the browser redirect. It is a modest custom module you control, and it works the same whether the customer pays in USDT, USDC, BTC, ETH, or SOL.

01

What 'accept crypto on Magento' actually means here

Be clear up front about what you are building, because it sets the whole approach. halfin does not ship a packaged Magento extension you install from the Marketplace, enable in the admin, and configure with an API key. What halfin provides is the underlying payment API and a hosted checkout page. Accepting crypto on Magento means integrating against that API the way you would integrate any off-site (redirect) payment method that hands off to an external page and reports back asynchronously — this is an integration pattern, not a halfin-supplied module.

Magento is built for exactly this. Its payment layer is an extension point: you register a payment method, give it a model that implements Magento's payment-method contract, and Magento renders it at checkout alongside your other methods. An off-site method that redirects to a hosted page and then receives a server-to-server callback is a shape Magento already supports — it is the same family as the redirect gateways Magento merchants use every day. You are not fighting the platform; you are filling in a small custom module with halfin's create-invoice call and a webhook controller.

The result is a payment method that appears at checkout, sends the customer to a halfin-hosted invoice to pay in the asset they hold, and flips the Magento 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 Magento 2 / Adobe Commerce store you can deploy a custom module to. Keep both secrets in Magento configuration — a core_config_data value set through the admin, or an environment variable read in your module — never hardcoded in a file you commit to the module's repository.

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 hosted 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 in Magento config or env — never in version control.
  • A Magento 2 / Adobe Commerce store and the ability to deploy a custom module.
  • A public route on your store that halfin can reach to POST webhook events.
03

Step 1 — Register a custom Magento payment method

Magento discovers payment methods through configuration, not magic. In your module you declare the method in etc/config.xml (its code, title, and an offline-redirect-style model), expose its admin settings in etc/adminhtml/system.xml, and back it with a model that implements Magento's payment-method contract — in modern Magento that is a vendor adapter wired through Magento\Payment\Model\Method\Adapter, or a class implementing Magento\Payment\Model\MethodInterface for a leaner method. Because halfin hosts the actual payment page, your method collects nothing on the checkout form itself; it just needs to mark the order as awaiting an off-site redirect and hand control to a controller that creates the invoice.

The shape below is the minimal config that makes a 'Pay with crypto' option appear at checkout and route the placed order to your redirect controller. Step 2 fills in the controller that calls halfin and produces the hosted checkout URL.

<!-- app/code/Halfin/Pay/etc/config.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
  <default>
    <payment>
      <halfin>
        <active>1</active>
        <title>Pay with crypto</title>
        <model>Halfin\Pay\Model\HalfinPayment</model>
        <!-- Off-site: place the order, then redirect to a hosted invoice. -->
        <order_place_redirect_url>halfin/checkout/redirect</order_place_redirect_url>
        <payment_action>order</payment_action>
      </halfin>
    </payment>
  </default>
</config>
04

Step 2 — Create a halfin invoice for the order

When the customer places the order with the crypto method, your redirect controller (the halfin/checkout/redirect route from Step 1) creates a halfin invoice and sends the browser to its hosted checkout page. Anchor the invoice to fiat: send the Magento order grand total as a string amount_fiat with the store's fiat_currency (USD, EUR, whatever the store view 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 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 Magento order increment id so that a customer who refreshes the redirect, or a retry from your side, returns the existing invoice instead of issuing a second bill for the same cart. Carry the order increment 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 controller 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 Magento's Magento\Framework\HTTP\ClientInterface (or Guzzle). Send only the documented headers: X-API-Key and Content-Type. The idempotency_key is a snake_case field in the request body, not a header.

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",
    "deferred": true,
    "description": "Magento order #000010472",
    "redirect_url": "https://store.example.com/halfin/checkout/return",
    "idempotency_key": "magento_order_000010472"
  }'

# The response carries the invoice id and a hosted checkout URL on
# checkout.thehalfin.com. Redirect the customer there from your controller.
# 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 the invoice's redirect_url to your Magento order-success page (the halfin/checkout/return route in the example) so the customer lands back on the store 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 success 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. Add a controller for it — a Magento controller action on a dedicated front-name route, mapped through your module's routes.xml and declared CsrfAware so Magento does not reject the external POST — and point your halfin webhook configuration at that URL.

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 (hash_equals in PHP), and only then read the event. Magento and PHP will happily hand you a parsed array — but compute the HMAC over the raw input (the request's getContent(), i.e. 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 increment id you carried in Step 2, load it with the order repository, and move it. On invoice.paid, register the captured payment and transition the order — create a Magento invoice for the order and set the order to processing (or complete for virtual goods), then let Magento fire its own downstream events. Keep the handler idempotent: webhook delivery is at-least-once, so a redelivered invoice.paid must not invoice the order twice or send a second confirmation email — guard on the order's existing state (already-paid orders are a no-op), which is exactly the property you want. Acknowledge fast with a 2xx and let Magento's own queues do the slow work.

  • Verify the HMAC signature over the raw body, in constant time (hash_equals), before acting — always.
  • Map the event to the Magento order via the increment id you attached to the invoice.
  • On invoice.paid, create the order invoice and move the order to processing/complete.
  • Stay idempotent — a redelivered paid event must transition the order exactly once.
  • Return 2xx fast; treat the customer's success-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. Magento 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 states Magento 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 invoice 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 on hold or pending_payment while it is merely confirming, and use Magento order comments / status history to record under- and overpayment so finance can act on it.

halfin webhook eventWhat it meansMagento order action
invoice.confirmingA matching deposit is seen and confirmations are accumulatingKeep the order on hold / pending_payment; do not invoice yet
invoice.paidConfirmed past the chain's threshold; settled to your balanceCreate the order invoice → move to processing/complete
invoice.underpaidA real payment arrived but is below the amount dueHold the order; add a status-history comment with the shortfall
invoice.overpaidSettled with a surplus over the amount dueInvoice the order; comment 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 method redirects to a hosted invoice, pay it on a test network, and watch your webhook controller receive the events and the Magento 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 success 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 webhook route — 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 is invoiced 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 Magento.

  • 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 invoice the order exactly once.
  • Expiry: a lapsed invoice must let the customer re-checkout cleanly.