Merchant API
商户接入Merchant integration
以美元标价,客户用 USDT 或 USDC 付款。 Price in US dollars. Customers pay in USDT or USDC.
- Base URL
- https://crenel.net
- 格式Format
- POST · application/json
- 开户Onboarding
- me@crenel.net
开户时提供商户名称和网站域名,拿到 merchant_id 与密钥。通知地址和跳回地址只能落在登记的域名下。密钥只放服务端。
Send your merchant name and site domains to receive a merchant_id and secret. Notification and redirect URLs must be on a registered domain. Keep the secret server-side.
01流程Flow
- 服务端调用
POST /api/v1/orders建单,拿到pay_url。Your server callsPOST /api/v1/ordersand receives apay_url. - 把客户带到
pay_url,客户在那里选网络、转账。Send the customer topay_urlto choose a network and transfer. - 到账核实后,Crenel 向你的
notify_url推送签名通知。验签后发货,回复ok。Once the payment is verified, Crenel posts a signed notification to yournotify_url. Verify it, fulfil the order, replyok. - 客户被带回
redirect_url。跳回只是界面,不代表已付款。The customer returns toredirect_url. The redirect is navigation only, not proof of payment.
02签名Signature
- 去掉
signature字段,以及值为 null 或空串的字段。Dropsignatureand every field whose value is null or an empty string. - 其余字段按键名升序排列,拼成
k=v&k=v。值不做 URL 编码;布尔写true/false。Sort the remaining keys ascending and join ask=v&k=v. No URL encoding; booleans aretrue/false. - 用商户密钥做 HMAC-SHA256,取小写十六进制,放进
signature。HMAC-SHA256 with your secret, lowercase hex, sent assignature.
请求和通知用同一套规则。金额一律传字符串("25.00"),timestamp 为 Unix 秒,与服务器相差不得超过 5 分钟。
Requests and notifications use the same rule. Send amounts as strings ("25.00"). timestamp is Unix seconds and must be within 5 minutes of server time.
const enc = new TextEncoder();
const canonical = (p) => Object.keys(p).sort()
.filter((k) => k !== 'signature' && p[k] !== null && p[k] !== undefined && p[k] !== '')
.map((k) => `${k}=${p[k]}`).join('&');
const hmacKey = (secret) => crypto.subtle.importKey(
'raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
async function sign(p, secret) {
const mac = await crypto.subtle.sign('HMAC', await hmacKey(secret), enc.encode(canonical(p)));
return Array.from(new Uint8Array(mac), (b) => b.toString(16).padStart(2, '0')).join('');
}
async function verify(p, secret) {
const hex = String(p.signature || '');
if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
const sig = Uint8Array.from(hex.match(/../g), (h) => parseInt(h, 16));
return crypto.subtle.verify('HMAC', await hmacKey(secret), sig, enc.encode(canonical(p)));
}
import hashlib, hmac
def canonical(p):
out = []
for k in sorted(p):
v = p[k]
if k == "signature" or v is None or v == "":
continue
if isinstance(v, bool):
v = "true" if v else "false"
out.append(f"{k}={v}")
return "&".join(out)
def sign(p, secret):
return hmac.new(secret.encode(), canonical(p).encode(), hashlib.sha256).hexdigest()
def verify(p, secret):
return hmac.compare_digest(str(p.get("signature", "")).lower(), sign(p, secret))
03建单Create order
POST /api/v1/orders
| 字段Field | 说明Description | |
|---|---|---|
| merchant_id | * | 开户时分配Issued at onboarding |
| order_id | * | 你的订单号,1–64 位字母、数字和 _ . : -,商户内唯一Your order ID: 1–64 letters, digits or _ . : -, unique per merchant |
| amount | * | 美元金额字符串,最多两位小数,上限 100000USD amount as a string, up to two decimals, max 100000 |
| currency | USD(默认,目前只支持它)USD (default and only value) | |
| notify_url | * | 接收到账通知的 HTTPS 地址HTTPS endpoint for payment notifications |
| redirect_url | 付款完成后带客户回去的 HTTPS 地址HTTPS page the customer returns to after paying | |
| name | 付款页上显示的商品名,最多 64 字Item name shown on the payment page, up to 64 characters | |
| remark | 你自己的备注或分类,最多 64 字,例如区分不同服务;通知和查单原样带回,客户看不到Your own note or category, up to 64 characters, e.g. to tell services apart; returned in notifications and queries, never shown to the customer | |
| ttl_seconds | 有效期,300–604800 秒,默认 1800Lifetime in seconds, 300–604800, default 1800 | |
| tokens | 只收某种币:USDT、USDC 或 USDC,USDT;不传即两种都收Restrict tokens: USDT, USDC or USDC,USDT; omit for both | |
| test | true 为测试单,付款页带「测试单」标记;不传即真实订单true marks a test order, labelled on the payment page; omit for a live order | |
| timestamp | * | Unix 秒Unix seconds |
| signature | * | 见上文See above |
同一 order_id 以相同金额重复提交,返回原来那张单;金额不同则返回 409。
Re-submitting an order_id with the same amount returns the original order; a different amount returns 409.
const body = {
merchant_id: 'shop',
order_id: 'A1001',
amount: '25.00',
currency: 'USD',
name: 'Order A1001',
remark: 'parts',
notify_url: 'https://shop.example/crenel/notify',
redirect_url: 'https://shop.example/orders/A1001',
timestamp: Math.floor(Date.now() / 1000),
};
body.signature = await sign(body, SECRET);
const res = await fetch('https://crenel.net/api/v1/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const { ok, data } = await res.json();
{
"ok": true,
"data": {
"trade_id": "q3JxV0b2nR8sKf1tW4yZcA",
"order_no": "9F2C41D7",
"pay_url": "https://crenel.net/pay/q3JxV0b2nR8sKf1tW4yZcA",
"status": "waiting",
"amount": "25.00",
"currency": "USD",
"expires_at": 1761040800,
"test": false,
"remark": "parts"
}
}
trade_id 是 Crenel 的订单标识;order_no 是给人看的 8 位订单号,即收款地址的末 8 位,付款页上客户看到的就是它。
trade_id identifies the order at Crenel. order_no is the 8-character reference customers see, the last 8 characters of the deposit address.
04付款页Payment page
- 直接跳转或链接到
pay_url,不要嵌进 iframe。Redirect or link topay_url. Framing is not allowed. - 语言:中文为默认,加
?lang=en显示英文。Chinese by default; append?lang=enfor English. - 付款成功后 4 秒自动回到
redirect_url;订单过期时页面也提供返回按钮。不传则停在付款页。After payment the page returns toredirect_urlwithin 4 seconds; expired orders show a back button. Without it the customer stays on the page.
05到账通知Notification
订单付清后,Crenel 以 JSON POST 到 notify_url。返回 2xx 且正文为 ok 才算送达。
When an order is paid, Crenel sends a JSON POST to notify_url. Delivery succeeds only on a 2xx response whose body is ok.
| 字段Field | 说明Description |
|---|---|
| merchant_id · order_id | 你建单时传的值As sent when creating the order |
| trade_id · order_no | Crenel 订单标识与 8 位订单号Crenel order ID and 8-character reference |
| status | paid |
| amount · currency | 订单金额(美元)Order amount (USD) |
| actual_amount | 实际到账,可能略高于或低于订单金额Amount received; may be slightly above or below the order amount |
| token · network | USDT / USDC;网络键名见下表USDT / USDC; network keys are listed below |
| tx_hash | 链上交易哈希On-chain transaction hash |
| paid_at | 核实到账的时间,Unix 秒Time the payment was confirmed, Unix seconds |
| late | true 表示过期后的宽限期内才到账true if paid during the grace period after expiry |
| remark | 可选。建单时填的 remarkOptional. The remark sent when creating the order |
| short | 可选。在容差内少付时出现,值为差额Optional. Present when accepted within tolerance; the shortfall |
| fee | 可选。含在 actual_amount 里的网络手续费Optional. Network fee included in actual_amount |
| timestamp · signature | 发送时间与签名Send time and signature |
{
"merchant_id": "shop",
"order_id": "A1001",
"trade_id": "q3JxV0b2nR8sKf1tW4yZcA",
"order_no": "9F2C41D7",
"status": "paid",
"amount": "25.00",
"currency": "USD",
"actual_amount": "25.00",
"token": "USDC",
"network": "base",
"tx_hash": "0x5c1e…",
"paid_at": 1761039712,
"late": false,
"remark": "parts",
"timestamp": 1761039731,
"signature": "…"
}
export default {
async fetch(req, env) {
const p = await req.json();
if (!(await verify(p, env.CRENEL_SECRET))) return new Response('bad signature', { status: 401 });
if (p.status === 'paid') {
// Look up p.order_id, compare p.amount, fulfil once per trade_id.
}
return new Response('ok');
},
};
- 验签时用收到的原始字段,包括
late这样的布尔值。Verify against the fields exactly as received, booleans such aslateincluded. - 同一张单可能收到不止一次通知,按
trade_id去重。A notification can arrive more than once; de-duplicate ontrade_id. - 送达失败后依次在 15 秒、1 分、5 分、15 分、1 小时、3 小时、6 小时、12 小时、24 小时后重试,约 2 天后停止。单次请求超时 10 秒。Failed deliveries are retried after 15 s, 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h, then stop after about two days. Each attempt times out after 10 s.
06查单Query
POST /api/v1/orders/query
传 merchant_id、trade_id 或 order_id(二选一)、timestamp、signature。返回订单状态、remark、test,以及已付时的 actual_amount token network tx_hash paid_at late,另有 expires_at。用于补漏,不能代替通知。
Send merchant_id, either trade_id or order_id, timestamp and signature. Returns the status, remark, test and, once paid, actual_amount token network tx_hash paid_at late, plus expires_at. Use it for reconciliation, not instead of notifications.
07订单规则Order rules
- waiting
- 等待付款Awaiting payment
- paid
- 已付清,已通知Paid and notified
- expired
- 到期未付清Expired unpaid
- 每张单有自己的收款地址,可以分几笔付,累计够数即算付清。Each order has its own deposit address. Several transfers add up; the order is paid once the total is reached.
- 过期后 60 分钟内到账的钱仍会入账,订单转为
paid并带late: true。Funds arriving within 60 minutes after expiry still count: the order becomespaidwithlate: true. - 为吸收交易所提币手续费,少付不超过订单金额的 2% 且不超过 1 美元时仍算付清,通知里带
short。To absorb exchange withdrawal fees, a shortfall of at most 2% of the order and at most $1 is accepted; the notification carriesshort. - 付清后又转来的钱、宽限期之后才到的钱不会自动入账,Crenel 会联系你确认退款或改记。Funds sent after an order is paid, or after the grace period, are not applied automatically; Crenel will contact you to refund or reassign them.
- 真实订单的收款地址只用一次,订单结束后不再分配;测试单的地址会回收。上线前请用
test: true联调。A live order's address is used once and never reassigned. Test-order addresses are recycled, so integrate withtest: true.
08错误Errors
{ "ok": false, "error": "…", "code": "bad_request" }
- 400
- 参数不对Invalid parameters
- 401
- 商户、时间戳或签名无效Bad merchant, timestamp or signature
- 404
- 订单不存在Order not found
- 409
order_id已用其他金额建过单,或有效期、币种不合规order_idreused with another amount, or invalid lifetime or tokens- 413
- 请求体超过 16 KBBody larger than 16 KB
- 429
- 每个 IP 每分钟最多 300 次More than 300 requests per minute per IP
error 是给人看的中文说明,程序请按 HTTP 状态码判断。
error is a human-readable message in Chinese. Branch on the HTTP status.
09网络Networks
| 网络Network | network | USDT | USDC | 订单金额Order amount |
|---|---|---|---|---|
| tron | ● | — | > $500 | |
| bsc | ● | ● | ||
| ethereum | ● | ● | > $50 | |
| polygon | ● | ● | ||
| arbitrum | ● | ● | ||
| base | — | ● | ||
| optimism | ● | ● | ||
| avalanche | ● | ● | ||
| xlayer | ● | ● | ||
| arc | — | ● | ||
| plasma | ● | — | ||
| sonic | — | ● | ||
| sei | — | ● | ||
| monad | ● | ● | ||
| celo | ● | ● | ||
| kaia | ● | — | ||
| kava | ● | — | ||
| xdc | — | ● | ||
| zksync | — | ● | ||
| berachain | ● | — | ||
| unichain | ● | ● | ||
| worldchain | — | ● | ||
| scroll | ● | — | ||
| ronin | — | ● | ||
| morph | ● | ● | ||
| stable | ● | — | ||
| tempo | ● | — | ||
| mantle | ● | ● | ||
| hyperevm | ● | — |
金额门槛之下的订单,付款页不列出该网络。 Networks with a threshold are not offered for smaller orders.