cURL
适合先在终端验证 API Key 和订单创建流程。
# 网关地址,正式环境换成你的域名,例如 https://pay.your-domain.com
BASE_URL="https://pay.example.com"
# 商户 API Key,只能放在服务端或本地终端,不能放到网页前端
MERCHANT_API_KEY="sk_live_xxx"
# 创建一笔支付订单。返回结果里会包含 pay_amount、deposit_address、checkout_url 等字段
# amount:用户原本要支付的金额,系统会在返回值里分配唯一后缀金额
# merchant_order_id:你自己系统里的订单号,建议每笔业务订单唯一
# customer_reference:你自己系统里的用户标识,方便后续对账和排查
# notify_url:支付成功后,网关会把 payment.paid 事件推送到这个地址
curl -s "$BASE_URL/v1/payment-orders" \
-H "authorization: Bearer $MERCHANT_API_KEY" \
-H "content-type: application/json" \
-d '{
"amount": "100",
"merchant_order_id": "order-1001",
"customer_reference": "user-123",
"notify_url": "https://merchant.example/webhooks/usdt"
}'
# 创建订单后,把返回里的 data.id 填到这里
ORDER_ID="po_xxx"
# 查询订单状态。业务系统可以用它主动核对订单是否已经 paid
curl -s "$BASE_URL/v1/payment-orders/$ORDER_ID" \
-H "authorization: Bearer $MERCHANT_API_KEY"
Node.js
Node.js 18+ 已内置 fetch。适合 Next.js、Express、NestJS 等服务端项目。
// 从环境变量读取配置,避免把真实密钥写进代码仓库
const BASE_URL = process.env.USDT_GATEWAY_URL;
const API_KEY = process.env.USDT_MERCHANT_API_KEY;
// 创建支付订单。这个函数应当在你的服务端调用,而不是浏览器里调用
async function createPaymentOrder() {
const res = await fetch(`${BASE_URL}/v1/payment-orders`, {
method: "POST",
headers: {
// Bearer Token 是商户身份凭证
"authorization": `Bearer ${API_KEY}`,
// 请求体使用 JSON 格式
"content-type": "application/json"
},
body: JSON.stringify({
// 用户原始订单金额,不需要自己拼接后缀
amount: "100",
// 你的业务订单号,用来做幂等和对账
merchant_order_id: "order-1001",
// 可选:你的用户 ID、邮箱、手机号脱敏值等
customer_reference: "user-123",
// 可选:这笔订单支付成功后优先通知这个地址
notify_url: "https://merchant.example/webhooks/usdt"
})
});
// 非 2xx 通常代表参数错误、API Key 错误或商户状态异常
if (!res.ok) throw new Error(await res.text());
// 返回 JSON,常用字段在 data 里面
return res.json();
}
// 主动查询订单,适合后台任务补偿或用户点击“我已付款”后核对
async function getPaymentOrder(orderId) {
const res = await fetch(`${BASE_URL}/v1/payment-orders/${orderId}`, {
headers: { "authorization": `Bearer ${API_KEY}` }
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
Python
适合 Django、FastAPI、Flask 等服务端项目。
import os
import requests
# 从环境变量读取网关地址和商户 API Key
# API Key 属于敏感信息,不要写死到代码里
BASE_URL = os.environ["USDT_GATEWAY_URL"]
API_KEY = os.environ["USDT_MERCHANT_API_KEY"]
def create_payment_order():
# 创建支付订单。这个接口应当由你的后端服务调用
response = requests.post(
f"{BASE_URL}/v1/payment-orders",
headers={
# Bearer Token 用来验证商户身份
"authorization": f"Bearer {API_KEY}",
# 请求体是 JSON
"content-type": "application/json",
},
json={
# 用户原始订单金额,网关会返回带后缀的 pay_amount
"amount": "100",
# 你的业务订单号,建议保证唯一
"merchant_order_id": "order-1001",
# 可选:你的用户标识,方便对账
"customer_reference": "user-123",
# 可选:支付成功后的回调地址
"notify_url": "https://merchant.example/webhooks/usdt",
},
# 设置超时,避免网络异常时请求一直卡住
timeout=20,
)
# 非 2xx 会抛出异常,便于上层记录错误日志
response.raise_for_status()
return response.json()
def get_payment_order(order_id):
# 主动查询订单状态,适合补偿任务或客服排查
response = requests.get(
f"{BASE_URL}/v1/payment-orders/{order_id}",
headers={"authorization": f"Bearer {API_KEY}"},
timeout=20,
)
response.raise_for_status()
return response.json()
PHP
适合 Laravel、ThinkPHP、WordPress 插件或传统 PHP 服务端。
<?php
// 从环境变量读取配置,避免把密钥提交到代码仓库
$baseUrl = getenv('USDT_GATEWAY_URL');
$apiKey = getenv('USDT_MERCHANT_API_KEY');
// 封装一个通用请求函数,后面创建订单、查订单都可以复用
function gateway_request($method, $path, $body = null) {
global $baseUrl, $apiKey;
// 目标地址,例如 https://pay.example.com/v1/payment-orders
$ch = curl_init($baseUrl . $path);
// authorization 头用于商户鉴权,content-type 表示请求体是 JSON
$headers = [
'authorization: Bearer ' . $apiKey,
'content-type: application/json',
'accept: application/json',
];
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
if ($body !== null) {
// PHP 数组转 JSON 后发送给网关
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 非 2xx 表示失败,直接抛出网关返回的错误内容
if ($status < 200 || $status >= 300) {
throw new Exception($raw);
}
// 正常返回 JSON,转成 PHP 数组使用
return json_decode($raw, true);
}
// 创建支付订单。注意 amount 是用户原始金额,不是带后缀金额
$order = gateway_request('POST', '/v1/payment-orders', [
'amount' => '100',
// 你的业务订单号,建议唯一
'merchant_order_id' => 'order-1001',
// 可选:你的用户标识
'customer_reference' => 'user-123',
// 可选:支付成功后的回调地址
'notify_url' => 'https://merchant.example/webhooks/usdt',
]);
Go
适合 Go 后端或内部服务直接对接。
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func createPaymentOrder() (map[string]any, error) {
// 从环境变量读取配置,避免把真实 API Key 写进源码
baseURL := os.Getenv("USDT_GATEWAY_URL")
apiKey := os.Getenv("USDT_MERCHANT_API_KEY")
// 请求体里的 amount 是用户原始金额,网关会返回带唯一后缀的 pay_amount
body, _ := json.Marshal(map[string]string{
"amount": "100",
// 你的业务订单号,建议每笔订单唯一
"merchant_order_id": "order-1001",
// 可选:你的用户标识,方便对账和客服排查
"customer_reference": "user-123",
// 可选:支付成功后推送 payment.paid 的地址
"notify_url": "https://merchant.example/webhooks/usdt",
})
// 创建 POST 请求,目标路径是 /v1/payment-orders
req, _ := http.NewRequest("POST", baseURL+"/v1/payment-orders", bytes.NewReader(body))
// authorization 用于商户鉴权,content-type 表示请求体是 JSON
req.Header.Set("authorization", "Bearer "+apiKey)
req.Header.Set("content-type", "application/json")
// 发送请求到 USDT Gateway
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
// 非 2xx 通常是参数错误、API Key 错误或商户状态异常
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("gateway returned %s", res.Status)
}
// 正常响应是 JSON,常用字段在 data 里面
var payload map[string]any
return payload, json.NewDecoder(res.Body).Decode(&payload)
}
Java
适合 Spring Boot、Micronaut、Quarkus 或普通 Java 11+ 项目。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class UsdtGatewayDemo {
// 从环境变量读取配置,避免把密钥写死在代码里
static final String BASE_URL = System.getenv("USDT_GATEWAY_URL");
static final String API_KEY = System.getenv("USDT_MERCHANT_API_KEY");
public static String createPaymentOrder() throws Exception {
// 请求体是 JSON 字符串。amount 是用户原始金额,网关会返回带唯一后缀的 pay_amount
String json = """
{
"amount": "100",
"merchant_order_id": "order-1001",
"customer_reference": "user-123",
"notify_url": "https://merchant.example/webhooks/usdt"
}
""";
// 创建 POST 请求,目标路径是 /v1/payment-orders
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/v1/payment-orders"))
// Bearer Token 用来验证商户身份
.header("authorization", "Bearer " + API_KEY)
// 告诉网关请求体是 JSON
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
// 发送请求并把响应体作为字符串取回
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// 非 2xx 表示失败,直接把网关错误抛给上层处理
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException(response.body());
}
// 正常情况下 response.body() 是 JSON 字符串
return response.body();
}
}
Webhook 验签
验签时必须使用原始请求体。签名头格式是 USDT-Signature: t=timestamp,v1=signature。
Node.js / Express
import crypto from "node:crypto";
import express from "express";
const app = express();
// Webhook Secret 来自商户后台,用来校验回调是否真的来自网关
const WEBHOOK_SECRET = process.env.USDT_WEBHOOK_SECRET;
// express.raw 很重要:验签必须使用原始 body,不能先被 JSON 中间件解析
app.post("/webhooks/usdt", express.raw({ type: "application/json" }), (req, res) => {
// usdt-signature 格式示例:t=1710000000,v1=abcdef...
const header = req.header("usdt-signature") || "";
// 从签名头里取出时间戳和签名值
const timestamp = header.match(/t=([^,]+)/)?.[1];
const signature = header.match(/v1=([^,]+)/)?.[1];
// 参与签名的数据格式是:timestamp + "." + 原始请求体
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${req.body.toString("utf8")}`)
.digest("hex");
// timingSafeEqual 可以减少时序攻击风险;比较前先检查长度,避免抛异常
const signatureBuffer = Buffer.from(signature || "");
const expectedBuffer = Buffer.from(expected);
const valid = signatureBuffer.length === expectedBuffer.length
&& crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
if (!valid) {
return res.status(400).send("invalid signature");
}
// 验签通过后再解析 JSON。event.type 可能是 payment.paid、withdrawal.paid 等
const event = JSON.parse(req.body.toString("utf8"));
// TODO: 根据 event.id 做幂等,避免重复回调导致重复发货
res.json({ ok: true });
});
Python / FastAPI
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = "whsec_xxx"
@app.post("/webhooks/usdt")
async def usdt_webhook(request: Request):
# 验签必须使用原始请求体,所以这里先读取 bytes
raw = await request.body()
# usdt-signature 格式示例:t=1710000000,v1=abcdef...
header = request.headers.get("usdt-signature", "")
parts = dict(item.split("=", 1) for item in header.split(",") if "=" in item)
timestamp = parts.get("t")
signature = parts.get("v1")
# 缺少时间戳或签名时,直接拒绝请求
if not timestamp or not signature:
raise HTTPException(status_code=400, detail="missing signature")
# 参与签名的数据格式是:timestamp + "." + 原始请求体
expected = hmac.new(
WEBHOOK_SECRET.encode(),
timestamp.encode() + b"." + raw,
hashlib.sha256,
).hexdigest()
# compare_digest 用于安全比较签名
if not signature or not hmac.compare_digest(signature, expected):
raise HTTPException(status_code=400, detail="invalid signature")
# 验签通过后再解析业务事件。生产环境请用 event.id 做幂等
return {"ok": True}
PHP
<?php
// Webhook Secret 来自商户后台
$secret = getenv('USDT_WEBHOOK_SECRET');
// 验签必须使用原始请求体,不能先 json_decode 再签名
$raw = file_get_contents('php://input');
// PHP 会把 usdt-signature 请求头放到 HTTP_USDT_SIGNATURE
$header = $_SERVER['HTTP_USDT_SIGNATURE'] ?? '';
// 从签名头里取出时间戳和签名值
preg_match('/t=([^,]+)/', $header, $t);
preg_match('/v1=([^,]+)/', $header, $v1);
$timestamp = $t[1] ?? '';
$signature = $v1[1] ?? '';
// 参与签名的数据格式是:timestamp + "." + 原始请求体
$expected = hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
// hash_equals 用于安全比较签名
if (!$signature || !hash_equals($expected, $signature)) {
http_response_code(400);
echo 'invalid signature';
exit;
}
// 验签通过后再解析 JSON。生产环境请用事件 ID 做幂等
$event = json_decode($raw, true);
echo json_encode(['ok' => true]);
接入检查表
- API Key 只放服务端环境变量,不放前端。
- 创建订单时传入唯一的
merchant_order_id。 - 收银台明确提醒用户必须精确转账
pay_amount。 - Webhook 验签使用原始 body,并用事件 ID 做幂等。
- 业务发货只认
payment.paid,不要只看用户截图。 - 上线前用小额 USDT 做真实链上测试。