E-commerce integration
Who this is for
Merchants on Shopify, WooCommerce or a custom storefront who want a "pay with USDT" option at checkout: the buyer selects it, sees payment details, and the order is marked paid automatically once payment confirms.
Before you start
- You have an API KEY (see Get started) and a collection address configured.
- Your server can receive webhooks (the platform's payment callback) and call the Open API (with the KEY).
- Decide who triggers fulfilment: drive the order state from the platform's payment callback, not from a button the buyer clicks.
Order creation happens server-side, never in the checkout page
The checkout page is a browser. Every order request comes from your store backend (KEY included there), and the checkout page only displays payment details or redirects.
The shape: a custom payment method plus a webhook
All three platforms work the same way underneath:
Buyer selects "Pay with USDT" at checkout
│
▼
Your store backend ──(with the API KEY)──▶ platform createOrder
│
▼
Show payment details (address + amount + chain) or redirect to the platform checkout
│
▼
Buyer pays ──▶ on-chain confirmation ──▶ platform calls your webhook
│
▼
Your store backend marks the order paid / triggers fulfilmentWooCommerce: a custom payment gateway
WooCommerce integrates through a payment gateway plugin. The core is creating the order in process_payment and a REST endpoint to receive the callback:
<?php
// Simplified: a WooCommerce custom payment gateway
class WC_Gateway_Web3SDK extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'web3sdk';
$this->method_title = 'USDT (Web3SDK)';
$this->has_fields = false;
parent::__construct();
}
public function process_payment($order_id) {
$order = wc_get_order($order_id);
// 1) Create the order on your store backend: call the Open API with the KEY
$resp = wp_remote_post(getenv('API_BASE') . '/api/pay/open/createOrder', [
'headers' => [
'X-Pay-Api-Key' => getenv('PAY_API_KEY'), // server side only
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'merchantOrderNo' => (string) $order_id, // idempotency key
'chainId' => 2,
'tokenId' => 1,
'amount' => (string) $order->get_total(), // decimal string in token units
'subject' => 'Order #' . $order_id,
'notifyUrl' => home_url('/?wc-api=web3sdk_notify'),
'returnUrl' => $this->get_return_url($order),
]),
'timeout' => 15,
]);
$data = json_decode(wp_remote_retrieve_body($resp), true);
if (empty($data['code']) && !isset($data['data']['order'])) {
wc_add_notice('USDT order failed, please retry', 'error');
return ['result' => 'failure'];
}
$o = $data['data']['order'];
// 2) Store the payment details on the order; show them after checkout
$order->update_meta_data('_web3_order_no', $o['orderNo']);
$order->update_meta_data('_web3_pay_address', $o['payAddress']);
$order->update_meta_data('_web3_pay_amount', $o['payAmountDisplay']);
$order->update_meta_data('_web3_chain', $o['chainKey']);
$order->save();
return ['result' => 'success', 'redirect' => $this->get_return_url($order)];
}
}Receiving the callback (verify, deduplicate, update the order):
add_action('woocommerce_api_web3sdk_notify', function () {
$raw = file_get_contents('php://input'); // raw body
$ts = $_SERVER['HTTP_X_PAY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_PAY_SIGNATURE'] ?? '';
$key = getenv('PAY_API_KEY');
$want = hash_hmac('sha256', $ts . $raw, $key); // timestamp + raw body
if (!hash_equals($want, $sig)) { // constant-time comparison
status_header(401); exit;
}
$p = json_decode($raw, true);
$order_id = intval($p['merchantOrderNo'] ?? 0);
$order = $order_id ? wc_get_order($order_id) : null;
if (!$order) { status_header(200); exit; } // not our order; still answer 2xx
// Idempotent: already handled? answer and return
if ($order->get_meta('_web3_tx_hash') === ($p['txHash'] ?? '')) {
status_header(200); exit;
}
// Only "confirmed" counts; and do not trust the callback alone — optionally query the API first
if (($p['status'] ?? 0) >= 3) {
$order->update_meta_data('_web3_tx_hash', $p['txHash'] ?? '');
$order->payment_complete($p['txHash'] ?? ''); // triggers WooCommerce's paid flow
}
status_header(200); exit; // 2xx stops the retries
});Shopify: a custom payment method
Shopify's custom payment method sends the buyer to a URL you control at checkout. There you show payment details, and once payment confirms you mark the order paid through the Order API:
// 1) Your server: create the order in the redirect landing page
app.get('/shopify/pay', async (req, res) => {
const { orderId, total } = req.query
const r = await fetch(`${process.env.API_BASE}/api/pay/open/createOrder`, {
method: 'POST',
headers: { 'X-Pay-Api-Key': process.env.PAY_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
merchantOrderNo: String(orderId),
chainId: 2, tokenId: 1, amount: String(total),
subject: `Shopify Order ${orderId}`,
notifyUrl: 'https://your-shop.com/shopify/notify',
}),
}).then((x) => x.json())
res.render('pay', { order: r.data.order })
})
// 2) On the platform callback, mark the Shopify order paid
app.post('/shopify/notify', express.raw({ type: '*/*' }), async (req, res) => {
const raw = req.body.toString('utf8')
if (!verify(raw, req.get('X-Pay-Timestamp'), req.get('X-Pay-Signature'))) {
return res.sendStatus(401)
}
const p = JSON.parse(raw)
if (isProcessed(p.merchantOrderNo)) return res.sendStatus(200) // idempotent
await fetch(`https://${process.env.SHOP}/admin/api/2024-07/orders/${p.merchantOrderNo}/transactions.json`, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': process.env.SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transaction: { kind: 'capture', status: 'success', amount: p.amountDisplay },
}),
})
markPaid(p.merchantOrderNo, p.txHash)
res.sendStatus(200)
})Shopify API versions and fields change; check the official docs. What is shown here is the integration shape.
Callback handling
Whichever platform you use, callbacks follow the same rules:
- Verify:
HMAC-SHA256(apiKey, timestamp + raw body), compared in constant time. - Idempotent: deduplicate on
merchantOrderNo(usually your store's order id) so you never ship twice. - Return fast: about a 10-second timeout; if updating the order is slow, persist first and act asynchronously.
- 2xx stops retries: a 2xx means you received it; any other status retries on the backoff schedule.
- Query is authoritative: verify with Query order before fulfilment.
FAQ
| Question | Answer |
|---|---|
| Can I drop a JS snippet in the checkout page to create the order? | No — that exposes the KEY. Order creation must be server-side |
| Do platform callbacks clash with the store's own webhooks? | No. The platform callback drives "order paid"; the store's own webhooks stay as they are |
amount or the store total? | The Open API wants a decimal string in token units, which is the store total in the same unit |
| What about price volatility? | Pricing is in a stablecoin, so you pass the order amount directly; no FX conversion involved |
| What if a callback is lost? | The platform retries; you can also reconcile on a schedule with the query API |
| How long is stock held? | For the order's validity window (expiresAt); after that it is void and you need not wait |
Related
- Create order · Query order · Callbacks and verification
- Next.js integration (common for custom storefronts)