Tích hợp cổng thanh toán
Base URL: . Mọi request/response dùng JSON.
Xác thực
Gửi API key (biến API_KEY trong .env) qua header. Chỉ gọi API từ máy chủ của bạn, không để lộ key ở trình duyệt.
Authorization: Bearer YOUR_API_KEY
Nhiều website
Mỗi website kết nối có API key, Webhook secret và Webhook URL riêng, tạo trong Quản trị → Website & API.
- Hoá đơn tạo bằng key của website nào thì thuộc website đó. Mỗi website chỉ xem được hoá đơn của mình.
- Webhook gửi về Webhook URL của website đó (trừ khi hoá đơn có
callback_urlriêng) và được ký bằng Webhook secret của website đó. order_idchỉ cần không trùng trong cùng một website.- Key trong biến môi trường
API_KEY/WEBHOOK_SECRETvẫn dùng được, gọi là website "Mặc định".
Kiểm tra key đang thuộc website nào.
Nút Gửi thử trong trang quản trị gửi một sự kiện ping có chữ ký tới Webhook URL. Website chỉ cần trả mã 2xx.
Tạo hoá đơn
| Trường | Mô tả |
|---|---|
| amount * | Số tiền dạng chuỗi, ví dụ "19.99" |
| currency | USD (mặc định), VND, EUR — tự quy đổi theo tỷ giá thời gian thực |
| order_id | Mã đơn của bạn. Gọi lại với cùng order_id sẽ trả về hoá đơn cũ (chống tạo trùng) |
| description | Hiển thị cho khách trên trang thanh toán |
| customer_email | Tuỳ chọn |
| methods | Giới hạn loại coin, ví dụ ["USDT_TRC20","BTC"]. Bỏ trống = tất cả đang bật |
| callback_url | URL nhận webhook cho hoá đơn này (mặc định DEFAULT_CALLBACK_URL) |
| success_url | Chuyển khách về đây sau khi trả xong |
| cancel_url | Nút "Quay lại cửa hàng" |
| metadata | Object JSON bất kỳ (user_id, product_id…) — được gửi lại trong webhook |
curl -X POST /api/v1/invoices \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "19.99",
"currency": "USD",
"order_id": "ORDER-1001",
"description": "Gói Premium 1 tháng",
"metadata": { "user_id": 42, "product_id": "premium" },
"callback_url": "https://nen-tang-cua-ban.com/webhooks/crypto",
"success_url": "https://nen-tang-cua-ban.com/thanh-cong"
}'
Phản hồi 201:
{
"id": "inv_Qm9X...",
"status": "new",
"amount": "19.99",
"currency": "USD",
"checkout_url": "/pay/inv_Qm9X...",
"methods": [{ "code": "USDT_TRC20", "coin": "USDT", "network": "TRON (TRC20)" }, ...],
"payment": null,
"expires_at": "..."
}
Chuyển khách đến checkout_url (redirect hoặc mở popup/iframe).
Xem hoá đơn
Phương thức thanh toán
| Mã | Coin / Mạng |
|---|---|
| USDT_TRC20 | USDT trên TRON — phí rẻ nhất, khuyên dùng |
| USDT_BEP20 | USDT trên BNB Smart Chain |
| USDT_ERC20 | USDT trên Ethereum |
| USDC_ERC20 | USDC trên Ethereum |
| ETH | Ethereum |
| BTC | Bitcoin (mỗi hoá đơn một địa chỉ riêng nếu dùng xpub) |
Trạng thái hoá đơn
| new | Vừa tạo, khách chưa chọn coin |
| awaiting_payment | Đã chọn coin, đang chờ tiền |
| confirming | Đã thấy giao dịch, đang chờ đủ số xác nhận |
| paid | Hoàn tất — mở khoá sản phẩm |
| expired | Hết hạn. Nếu tiền đến muộn (trong LATE_PAYMENT_GRACE_MINUTES) vẫn tự chuyển sang paid |
Webhook
Mỗi khi trạng thái thay đổi, cổng gửi POST JSON đến callback_url. Sự kiện: invoice.confirming, invoice.paid, invoice.expired. Trả về HTTP 2xx để báo đã nhận; nếu không, cổng tự gửi lại (30s, 2m, 10m, 30m, 1h, 3h, 6h, 12h, 24h).
POST /webhooks/crypto
X-CryptoPay-Event: invoice.paid
X-CryptoPay-Signature: t=1762400000,v1=5f2b...e9
{
"id": "evt_8c1f...",
"event": "invoice.paid",
"created_at": "2026-11-06T03:00:00.000Z",
"data": {
"id": "inv_Qm9X...",
"status": "paid",
"order_id": "ORDER-1001",
"amount": "19.99",
"currency": "USD",
"metadata": { "user_id": 42, "product_id": "premium" },
"payment": {
"method": "USDT_TRC20", "coin": "USDT", "network": "TRON (TRC20)",
"address": "TXyz...", "amount": "19.9901",
"txid": "a1b2...", "confirmations": 1, "explorer_url": "https://tronscan.org/#/transaction/a1b2..."
},
"paid_at": "2026-11-06T03:00:00.000Z"
}
}
Webhook có thể đến nhiều lần — hãy xử lý idempotent theo data.id (chỉ mở khoá một lần).
Xác minh chữ ký
Chữ ký = HMAC_SHA256(WEBHOOK_SECRET, t + "." + raw_body). Luôn dùng body gốc (chưa parse).
// Node.js / Express
import crypto from 'node:crypto';
app.post('/webhooks/crypto', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString();
const { t, v1 } = Object.fromEntries(req.get('x-cryptopay-signature').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(`${t}.${raw}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) || Math.abs(Date.now()/1000 - t) > 300)
return res.status(401).end();
const evt = JSON.parse(raw);
if (evt.event === 'invoice.paid') unlockProduct(evt.data.order_id, evt.data.metadata);
res.json({ ok: true });
});
<?php // PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_CRYPTOPAY_SIGNATURE']), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $raw, getenv('WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1']) || abs(time() - (int)$sig['t']) > 300) { http_response_code(401); exit; }
$evt = json_decode($raw, true);
if ($evt['event'] === 'invoice.paid') unlock_product($evt['data']['order_id']);
echo 'ok';
Luồng tích hợp
- Khách bấm "Mua" → máy chủ của bạn gọi
POST /api/v1/invoicesvớiorder_id. - Chuyển khách tới
checkout_url. - Nhận webhook
invoice.paid→ xác minh chữ ký → mở khoá sản phẩm choorder_id. - (Dự phòng) Khi khách quay về
success_url, gọiGET /api/v1/invoices/:idđể kiểm tra lại trạng thái.