How to Accept USDT and USDC Payments on Your Website
A step-by-step guide to stablecoin payments for orders and invoices: create a payment, use a hosted payment page and fulfil the order from a webhook.
Stablecoins are the easiest way to take crypto for goods and services: a USDT or a USDC is worth a dollar, so the price you show is the price you get. This guide walks through a complete checkout, from the customer pressing Pay to your server marking the order as paid.
What a crypto checkout has to do
- Give the customer an address and an exact amount to send.
- Know which order a transfer belongs to.
- Wait until the transfer is confirmed, then mark the order paid, once.
- Handle the customer who never pays, pays late or pays too little.
The simplest way to know which order a transfer belongs to is to give every order its own address. That is what a one-time payment is: a request for one amount, with an address that is created for it and used for nothing else.
Step 1: create a payment for the order
Create a free account, open the One-Time Payments project in the dashboard and copy its API key. When a customer checks out, create a payment with the price in dollars. external_id is your order id.
curl https://api.lirchain.com/merchant/one-time-payment/create \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"network": "polygon",
"asset": "USDT",
"amount": 49.5,
"external_id": "order-1042"
}'const API_KEY = "YOUR_API_KEY";
async function createPayment(params) {
const response = await fetch("https://api.lirchain.com/merchant/one-time-payment/create", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`LirChain API ${response.status}: ${await response.text()}`);
}
return response.json();
}
const result = await createPayment({
network: "polygon",
asset: "USDT",
amount: 49.5,
external_id: "order-1042",
});const API_KEY = "YOUR_API_KEY";
type CreatePaymentParams = {
network: string;
asset: string;
amount?: number;
receive_amount?: number;
external_id?: string;
};
type CreatePaymentResult = {
id: string;
external_id: string | null;
network: string;
address: string;
asset: string;
amount: number;
pay_amount: number;
receive_amount: string;
status: string;
fee: string;
created_ms: number;
expire_ms: number;
};
async function createPayment(params: CreatePaymentParams): Promise<CreatePaymentResult> {
const response = await fetch("https://api.lirchain.com/merchant/one-time-payment/create", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`LirChain API ${response.status}: ${await response.text()}`);
}
return response.json();
}
const result = await createPayment({
network: "polygon",
asset: "USDT",
amount: 49.5,
external_id: "order-1042",
});import requests
API_KEY = "YOUR_API_KEY"
def create_payment(**params):
response = requests.post(
"https://api.lirchain.com/merchant/one-time-payment/create",
headers={"x-api-key": API_KEY},
json=params,
timeout=30,
)
response.raise_for_status()
return response.json()
result = create_payment(
network="polygon",
asset="USDT",
amount=49.5,
external_id="order-1042",
)<?php
const API_KEY = "YOUR_API_KEY";
function create_payment(array $params): array
{
$curl = curl_init("https://api.lirchain.com/merchant/one-time-payment/create");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ["x-api-key: " . API_KEY, "content-type: application/json"],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($body === false || $status >= 400) {
throw new Exception("LirChain API $status: $body");
}
return json_decode($body, true);
}
$result = create_payment([
"network" => "polygon",
"asset" => "USDT",
"amount" => 49.5,
"external_id" => "order-1042",
]);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const apiKey = "YOUR_API_KEY"
func CreatePayment(params map[string]any) (map[string]any, error) {
body, err := json.Marshal(params)
if err != nil {
return nil, err
}
request, err := http.NewRequest("POST", "https://api.lirchain.com/merchant/one-time-payment/create", bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("x-api-key", apiKey)
request.Header.Set("content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode >= 400 {
return nil, fmt.Errorf("LirChain API %d: %s", response.StatusCode, data)
}
var result map[string]any
return result, json.Unmarshal(data, &result)
}
func main() {
result, err := CreatePayment(map[string]any{
"network": "polygon",
"asset": "USDT",
"amount": 49.5,
"external_id": "order-1042",
})
fmt.Println(result, err)
}You choose the stablecoin (USDT or USDC) and the network (Ethereum, BNB Chain or Polygon). Many shops let the customer pick and create the payment after that. BNB Chain and Polygon are the cheap ones for the customer to send on.
The answer has the payment's id, its address, pay_amount (what the customer has to send) and receive_amount (what reaches your balance after the fee). If you would rather fix what you receive, send receive_amount instead of amount and the customer's amount is worked out for you.
Step 2: send the customer to pay
Redirect the customer to lirchain.com/pay/<payment id>. The hosted page shows the amount, the address with a QR code, the network and a countdown, and updates by itself when the payment arrives. If you want the payment inside your own checkout, show the address and pay_amount yourself and poll the payment's status.
A payment is paid by one transfer of at least the full amount. Tell customers to send the exact amount in one go, and on the network shown.
Step 3: fulfil the order from the webhook
Open the project in the dashboard, paste the HTTPS address of your endpoint into its webhook field and save. When the payment becomes paid, this is posted to it:
{
"type": "one-time-payment",
"payment": {
"id": "ccf1c790-9d3d-4cda-98a9-5cd590a102df",
"external_id": "order-1042",
"project_id": "3d6b1130-da92-4ddf-bad2-93f898993c39",
"network": "polygon",
"address": "0x20d5b8802fc43b24cdbc9cdaa59dd6690626ad2c",
"asset": "USDT",
"contract": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
"amount": 49.5,
"pay_amount": 49.5,
"receive_amount": "49.2525",
"received": "49.5",
"status": "paid",
"fee": "0.2475",
"created_ms": 1791503932667,
"expire_ms": 1791507532667
}
}import express from "express";
const app = express();
app.use(express.json());
const API_KEY = process.env.LIRCHAIN_API_KEY;
// Use a table in your database instead of memory in production
const handled = new Set();
app.post("/webhooks/lirchain", async (req, res) => {
const id = req.get("x-webhook-id");
if (handled.has(id)) {
return res.sendStatus(200);
}
handled.add(id);
if (req.body.type === "one-time-payment") {
// The webhook is a signal. The API is the proof.
const response = await fetch(
`https://api.lirchain.com/merchant/one-time-payment/otp?id=${encodeURIComponent(req.body.payment.id)}`,
{ headers: { "x-api-key": API_KEY } },
);
const payment = await response.json();
if (response.ok && payment.status === "paid") {
console.log(`order ${payment.external_id} is paid`);
}
}
res.sendStatus(200);
});
app.listen(3000);Answer with a 2xx status within 10 seconds. If your server does not, the same webhook is sent again every minute, up to 20 times, each time with the same x-webhook-id header, so keep the ids you handled and skip repeats.
Customers who do not pay
A payment gives the customer one hour (expire_ms). The hosted page shows the time left and says when it has run out. A payment that is never paid costs you nothing. If money arrives that does not complete a payment, for example a smaller amount, it is still added to your balance, so nothing is lost and you can settle with the customer.
What it costs
- Creating a payment is free. A paid payment costs the percentage of your plan, from 1% on Free down to 0.25% on Enterprise, at least $0.10.
- The Free plan includes 1,000 payments per 30 days, with no card needed to start.
- What you receive is added to one balance in dollars, which you withdraw as USDT or USDC from the dashboard.
Questions and answers
- Do I need a company or a merchant account to accept USDT?
- You need a LirChain account, which takes an email address. New accounts start on the Free plan.
- Which networks can customers pay on?
- USDT and USDC on Ethereum (ERC-20), BNB Chain (BEP-20) and Polygon. You choose the stablecoin and the network for every payment.
- How long until a payment is marked as paid?
- Once the transfer is 12 blocks deep: a few seconds to under a minute on BNB Chain and Polygon, and a few minutes on Ethereum.
Try it on the Free plan
Every product is included, and no card is needed to sign up.
Create a free account