Integrating Cryptocurrency Payments into E-Commerce Stores
Common mistake when integrating crypto into e-commerce: treating it as just another payment method in existing checkout. In reality it's different flow — crypto payments have no instant finality (except L2), no chargebacks, rate changes while user walks to wallet, and partial payment is real edge case, not theoretical.
Choosing Approach: Hosted vs Custom
For e-commerce with volume of few hundred payments/month, using ready-made solutions makes sense — NOWPayments, CoinGate, BitPay. They take 0.5–1% fee but free you from infrastructure tasks.
Custom integration justified when:
- Need specific coin/network set that provider doesn't support
- Privacy requirements (don't want third party seeing transactions)
- High volumes where provider fees are significant
- Specific logic (e.g., auto-conversion via DEX)
Below is custom integration, since it requires more technical solutions.
WooCommerce: Custom Payment Gateway Plugin
WooCommerce provides abstract WC_Payment_Gateway class — just extend it:
class WC_Crypto_Gateway extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'crypto_payment';
$this->title = 'Pay with Cryptocurrency';
$this->method_description = 'Bitcoin, Ethereum, USDT and more';
$this->supports = ['products'];
$this->init_form_fields();
$this->init_settings();
add_action('woocommerce_update_options_payment_gateways_' . $this->id,
[$this, 'process_admin_options']);
add_action('woocommerce_api_crypto_payment', [$this, 'handle_webhook']);
}
public function process_payment($order_id): array {
$order = wc_get_order($order_id);
// Create payment in external service or generate address
$payment = $this->create_crypto_payment($order);
// Save data for instruction display
$order->update_meta_data('_crypto_payment_id', $payment['id']);
$order->update_meta_data('_crypto_pay_address', $payment['address']);
$order->update_meta_data('_crypto_pay_amount', $payment['amount']);
$order->update_meta_data('_crypto_expires_at', $payment['expires_at']);
$order->set_status('pending', 'Awaiting cryptocurrency payment');
$order->save();
return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
public function handle_webhook(): void {
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PAYMENT_SIGNATURE'] ?? '';
if (!$this->verify_signature($payload, $signature)) {
wp_die('Invalid signature', 401);
}
$data = json_decode($payload, true);
$order = wc_get_order($data['order_id']);
if (!$order) wp_die('Order not found', 404);
if ($data['status'] === 'confirmed') {
$order->payment_complete($data['transaction_hash']);
$order->add_order_note(
sprintf('Crypto payment confirmed. TX: %s', $data['transaction_hash'])
);
}
wp_die('OK', 200);
}
}
Thank you page (after redirect) should show address, QR code, and sum with timer. WooCommerce calls get_return_url() which leads to standard thank you page — customize via woocommerce_thankyou_{gateway_id} action.
Shopify: Using Payment Apps API
Shopify doesn't allow arbitrary custom PHP. For crypto integration create Shopify App via Partner Dashboard, use Payments Apps API.
Principle: your app registers as payment provider. On checkout, Shopify makes HTTP request to your endpoint with order data, you return redirect URL to your payment page, and after confirmation send resolved/rejected via GraphQL mutation.
// Shopify calls this endpoint
app.post('/shopify/payment', async (req, res) => {
const { gid, amount, currency, cancelUrl, kind } = req.body;
// Create internal payment
const payment = await createCryptoInvoice({
shopifyOrderGid: gid,
fiatAmount: parseFloat(amount),
fiatCurrency: currency,
});
// Redirect to our payment page
res.json({
redirect_url: `${process.env.APP_URL}/pay/${payment.id}`,
});
});
// After payment confirmation
async function notifyShopifyPaymentComplete(paymentGid: string, txHash: string) {
const mutation = `
mutation PaymentSessionResolve($id: ID!) {
paymentSessionResolve(id: $id) {
paymentSession {
id
state { ... on PaymentSessionStateResolved { code } }
}
userErrors { field message }
}
}
`;
await shopifyGraphQL(mutation, { id: paymentGid });
}
Rate and Expiration: Critical UX Details
User sees $99 price, clicks "pay with crypto", lands on page with 0.0271 ETH. This sum is valid for 15–30 minutes. If user hesitates or rate changes significantly — need refresh mechanism.
Timer on payment page should be functional — on expiration auto-refresh invoice:
// Client code
let expiresAt = new Date(invoice.expiresAt);
const timer = setInterval(async () => {
const remaining = expiresAt.getTime() - Date.now();
if (remaining <= 0) {
clearInterval(timer);
// Request new invoice with current rate
const refreshed = await fetch(`/api/payment/${invoiceId}/refresh`, {
method: 'POST'
});
const newInvoice = await refreshed.json();
expiresAt = new Date(newInvoice.expiresAt);
updateUI(newInvoice); // Update QR and sum
}
}, 1000);
On backend at refresh — recalculate crypto sum with current rate, update DB record, same address (if using unique address per payment).
Order Status and Customer Communication
| Order Status | Trigger | Email to Customer |
|---|---|---|
pending |
Invoice created | "Awaiting payment, address: X" |
processing |
Transaction found, waiting for confirmations | "Payment found, confirming" |
paid |
Confirmations reached | "Payment confirmed" |
partially_paid |
Incomplete sum received | "Incomplete amount received, contact us" |
expired |
Timer expired without payment | "Invoice expired, create new one" |
Reconciliation and Reporting
For accounting need crypto-to-fiat conversion at receipt time. Fix in DB: crypto_amount, crypto_currency, fiat_amount, fiat_currency, exchange_rate, confirmed_at. Rate source — Chainlink (on-chain) or CoinGecko API (off-chain) with timestamp. Critical for tax accounting.







