Guide

How to accept crypto payments on a Shopify store

Shopify locks the payment step. Custom payment methods that take money inside the native checkout are gated behind Shopify Payments and approved gateways, and there is no official halfin Shopify app that slots into that flow. That does not block you — it just decides the shape of the integration. This guide shows the honest pattern: send the buyer to a halfin invoice (a hosted checkout link or one you place yourself), let them pay in BTC, USDT, USDC, ETH, or SOL, and reconcile the Shopify order from a signed webhook. It is an integration pattern built on the public API, not a packaged plugin — and saying that plainly is the point, because anything claiming to be a one-click Shopify crypto plugin for halfin would be inventing a product that does not exist.

01

There is no official halfin Shopify plugin — and what that means

Start here so nothing downstream surprises you. halfin does not ship a Shopify app, and Shopify does not let an arbitrary third party inject a payment method into its native checkout — that surface is reserved for Shopify Payments and a short list of approved gateways. So you will not find a halfin entry in the Shopify payment-provider list, and you should be suspicious of anything that claims you can drop in crypto-at-checkout for halfin with one toggle.

What you can do is well-trodden and reliable: treat the crypto payment as a step that happens on a halfin-owned surface, then bring the result back into Shopify. The buyer ends up on a halfin invoice — either the hosted checkout page on checkout.thehalfin.com or a payment screen you render against the same API — pays from their wallet, and your backend marks the matching Shopify order paid once a signed webhook confirms it. Everything in this guide is that one idea, made concrete.

Two integration shapes cover almost every store. The link approach is the lightest: you generate a halfin invoice and hand the customer its checkout URL, by email, on a thank-you page, or as a manual order. The API-driven approach is tighter: a small service of yours creates the invoice when an order is placed, redirects the buyer, and listens for the webhook to flip the order's financial status. Pick based on how much engineering you want to own; both use the same primitives.

  • No halfin app in the Shopify payment-provider list — native checkout is gated to Shopify Payments and approved gateways.
  • Crypto is paid on a halfin surface (hosted checkout or your own page), not inside Shopify's checkout.
  • Shopify's order is reconciled afterward from a signed webhook — the order is the system of record on the storefront side.
  • This is an API + hosted-checkout integration pattern, not a packaged plugin. Treat any 'one-click halfin Shopify plugin' claim as false.
02

What you need before you start

Three things on the halfin side and one on the Shopify side. On halfin: a merchant account with a scoped API key for creating invoices, a webhook endpoint registered with its signing secret, and a decision about which assets and networks you are comfortable settling. On Shopify: a way to write back to the order — either a private/custom app with the orders scope so your service can mark an order paid through the Admin API, or, for the lightest setup, manual orders you reconcile by hand.

Decide your rail list up front because it shapes the buyer's choice at pay time. A general e-commerce audience tends to reach for stablecoins on low-fee networks — USDC on Base or Solana, USDT on Tron — while some customers will want to pay BTC or ETH directly. halfin settles the real supported surface below; enable the ones that match how your buyers actually hold value and how you want to reconcile.

AssetNetworks halfin settlesWhy a store enables it
USDTTron (TRC-20), Ethereum (ERC-20), SolanaMost common stablecoin; low fees on Tron and Solana
USDCEthereum (ERC-20), Base, SolanaDollar-stable checkout for US / EVM-native buyers
BTCBitcoinBuyers paying from cold storage or BTC-only wallets
ETHEthereum + ERC-20EVM-native buyers paying in the asset they hold
SOLSolana (SOL + SPL)Fast, low-fee settlement
Native L2 / chain assetsBase, Arbitrum, Polygon, BNB Smart ChainLet buyers pay where they already hold value
03

Step 1 — Create a halfin invoice anchored to the order total

Your Shopify order total is in fiat — a cart is $129.00, not 129-dollars-of-whatever-crypto-is-today. So anchor the invoice to fiat: send the order total as a string amount_fiat with the store's fiat_currency (USD, EUR, whatever your shop is priced in), and let halfin quote and lock the crypto amount. The rate locks when the invoice activates and the quote carries an expiry, so the buyer pays the dollar value of the cart even if the asset's price moves while the payment page is open. If the quote window lapses, you create a fresh invoice rather than honoring a stale rate. Do not send a crypto code as the currency and do not pre-pick the asset — the hosted page lets the buyer choose how to pay.

Carry the Shopify order identifier on the invoice — put the order id (and order number) in the description or a field you control so the webhook that comes back later tells you exactly which order to mark paid, with no fuzzy amount-matching. Derive an idempotency key from that same order id so a retried create call from your app returns the existing invoice instead of billing the buyer twice for one cart.

The curl below creates a cart invoice against the public API. Amounts are sent as strings — monetary values are strings end to end, never floats — and fiat_currency is your fiat anchor (a fiat code), while currency would only ever be a crypto code for a fixed-asset invoice, which is not what a Shopify cart wants. The same call works through the @halfin/sdk-merchant TypeScript client if your service is in TypeScript. The exact request and response fields live in the API reference at docs.thehalfin.com; what matters here is that one authenticated call returns an invoice id and a hosted checkout URL.

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": "129.00",
    "fiat_currency": "USD",
    "description": "Order #1042 — halfin checkout",
    "idempotency_key": "shopify-order-4821"
  }'

# The response carries the invoice id and a hosted checkout URL on
# checkout.thehalfin.com. Store the invoice id against the Shopify order id,
# redirect (or email) the buyer to the URL, and wait for the signed
# webhook before fulfilling. See docs.thehalfin.com for the full schema.
04

Step 2 — Get the buyer to the payment surface

Once the invoice exists you need the buyer in front of it. The lightest option is the hosted checkout: the create-invoice response includes a checkout URL on checkout.thehalfin.com, and you send the buyer there — from a custom thank-you page, an order-confirmation email, or a 'Pay with crypto' button on a manual order. halfin owns the wallet flows, the QR code, the deposit address, and the live payment status, so you build almost nothing.

If you want the payment screen to feel native to your store, render your own self-hosted checkout against the same invoice and API instead of redirecting off-site. You keep the page; halfin keeps the rails. Either way the invoice object is identical, so you can start with the hosted link and move to a self-hosted page later without changing how you create invoices or reconcile orders.

Where this slots into Shopify depends on your store. The most common shapes: a custom 'Pay with crypto' button on the cart or a post-purchase page that calls your service to create the invoice and redirect; or a draft/manual order workflow where staff send the buyer a checkout link. What you do not do is try to embed crypto-at-pay inside Shopify's native checkout step — that surface is closed, and pretending otherwise is where integrations break.

  • Hosted checkout: redirect or email the checkout.thehalfin.com URL from the create-invoice response — least code.
  • Self-hosted checkout: render your own page against the same invoice if it must live inside your store's look.
  • Wire it as a custom button / post-purchase page / manual-order link — never inside Shopify's native checkout step.
  • The invoice object is the same for both surfaces, so you can switch later without reworking creation or reconciliation.
05

Step 3 — Don't fulfill on the redirect; wait for confirmation

A crypto payment is not final the instant a wallet says 'sent'. The transaction has to land in a block and accumulate enough confirmations that a chain reorganization is no longer a realistic risk. Each chain has its own threshold and pace, and halfin applies a per-chain confirmation threshold with reorg-aware crediting — an invoice it reports as paid has settled under that chain's rules, not merely been seen in the mempool.

For Shopify this has one hard consequence: do not mark the order paid or trigger fulfillment when the buyer is bounced back to your thank-you page. The success redirect can also simply be missed — a buyer pays on their phone, the wallet app foregrounds, the browser tab is gone, and your return page never loads. If the redirect is your only signal, you will silently fail to record orders that were actually paid. Key the order's status off the webhook in Step 4, not off the buyer's browser.

Two payment edge cases deserve a defined policy because Shopify's paid/unpaid model ignores them. An underpaid invoice — the buyer sent slightly too little, often because they covered the network fee out of the same amount — should not auto-fulfill; halfin records the shortfall against the quote so your team can request the remainder or void the order. An overpaid invoice records the excess, which you can refund or credit. Decide the policy once and let the recorded invoice state drive it rather than discovering mismatches in a month-end reconciliation.

Invoice eventWhat it meansWhat to do with the Shopify order
invoice.confirmingInvoice is live with a locked quote and an expiryLeave the order unpaid; show the buyer the amount and address
invoice.paidConfirmed past the chain's thresholdMark the order paid and release fulfillment
invoice.underpaidLess than the quote was receivedHold the order; request the remainder or void — do not fulfill
invoice.overpaidMore than the quote was receivedFulfill and flag the surplus for refund or store credit
invoice.expiredThe quote window closed before paymentLeave the order unpaid; issue a fresh invoice if the buyer returns
06

Step 4 — Reconcile the Shopify order from a signed webhook

This is the step that makes the integration trustworthy, so be strict about it. halfin POSTs an HMAC-signed event to your endpoint whenever an invoice changes state, and that event arrives independently of whatever the buyer's browser did. Treat it as the source of truth for the order's financial status. The events you care about for a storefront are invoice.confirming, invoice.paid, invoice.underpaid, invoice.overpaid, and invoice.expired — note there is no invoice.activated event; the live-and-awaiting-payment signal is invoice.confirming.

Verify the signature before you trust anything. Your endpoint URL is public the moment you register it, so anyone can POST forged JSON at it. Recompute the HMAC over the exact raw request bytes — before any framework middleware reparses the body — and compare it to the signature header with a constant-time comparison. Only after that check passes do you parse the body and act. An unsigned or mismatched request is hostile: return a 4xx and do nothing. Marking a Shopify order paid off an unverified 'paid' payload is exactly how a store ships goods against money that never arrived.

Once verified, look up the Shopify order by the identifier you attached in Step 1 and write the status back through Shopify's Admin API — mark the order paid on invoice.paid and let your fulfillment rules take over. Keep the handler idempotent: delivery is at-least-once, so the same paid event can arrive more than once with the same stable event id, and a redelivery must not mark the order paid twice or fire a second confirmation email. Dedupe on the event id, return 2xx quickly, and push the Shopify write and any slow work onto a queue.

The Node sketch below is the spine: capture the raw body, verify the HMAC, dedupe on the event id, then branch on the event type. The Shopify Admin API call to flip the order's financial status goes where the comment marks it; that part is standard Shopify, not halfin-specific.

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const SIGNING_SECRET = process.env.HALFIN_WEBHOOK_SECRET!;
const seen = new Set<string>(); // back this with a durable store in production

app.post(
  "/webhooks/halfin",
  // Raw body — HMAC must be computed over the exact bytes received,
  // not a re-serialized JSON object.
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.header("x-halfin-signature") ?? "";
    const expected = createHmac("sha256", SIGNING_SECRET)
      .update(req.body) // req.body is a Buffer here
      .digest("hex");

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.status(401).send("invalid signature");
    }

    const event = JSON.parse(req.body.toString("utf8"));
    if (seen.has(event.id)) return res.status(200).send("ok"); // dedupe
    seen.add(event.id);

    if (event.type === "invoice.paid") {
      // Read back the order id you stored against the invoice in Step 1.
      // The exact event field names are in the API reference at docs.thehalfin.com.
      const shopifyOrderId = orderIdFromInvoice(event);
      // Mark the Shopify order paid via the Admin API, then let fulfillment run.
      // await markShopifyOrderPaid(shopifyOrderId);
    }
    // invoice.underpaid / invoice.overpaid / invoice.expired -> your own policy.

    return res.status(200).send("ok"); // ack fast; do slow work on a queue
  },
);
07

Step 5 — Refunds, manual orders, and operating the flow

A crypto sale still needs a refund path. halfin refunds let you return funds for a paid invoice; trigger one from your support flow when a Shopify order is refunded or cancelled, and reconcile the result through the same signed webhook stream you already consume. Keep the Shopify refund and the halfin refund linked by the order id so finance can see both halves of the transaction.

If you do not want to build a service at all, the manual route works for low volume. Create an invoice in the halfin dashboard for the cart total, send the buyer the checkout link, and once the dashboard (or the webhook) shows it paid, mark the matching Shopify order paid by hand. It does not scale, but it is a legitimate way to start accepting crypto on a Shopify store before you invest in the API-driven flow.

However you wire it, the operating discipline is the same as any webhook integration: store a payouts- or refund-capable key separately from your read-only and invoicing keys so a leaked reporting credential can never move money, log the event id and verification result for every webhook so you can answer 'why didn't order #1042 fulfill', and re-read the invoice from the API by id when you want a definitive state. The combination — one invoice per order, one verified paid event behind every fulfillment, one refund record per return — is what keeps the Shopify side and the halfin side reconcilable.

  • Use halfin refunds for returns; link the halfin refund to the Shopify order id and reconcile via webhook.
  • Manual route: dashboard invoice + emailed checkout link + by-hand order update — fine for low volume.
  • Scope API keys narrowly; never let an invoicing or reporting key also move money.
  • Log every webhook's event id and verification result so order-level questions are answerable.