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

  1. 服务端调用 POST /api/v1/orders 建单,拿到 pay_url。Your server calls POST /api/v1/orders and receives a pay_url.
  2. 把客户带到 pay_url,客户在那里选网络、转账。Send the customer to pay_url to choose a network and transfer.
  3. 到账核实后,Crenel 向你的 notify_url 推送签名通知。验签后发货,回复 ok。Once the payment is verified, Crenel posts a signed notification to your notify_url. Verify it, fulfil the order, reply ok.
  4. 客户被带回 redirect_url。跳回只是界面,不代表已付款。The customer returns to redirect_url. The redirect is navigation only, not proof of payment.

02签名Signature

  1. 去掉 signature 字段,以及值为 null 或空串的字段。Drop signature and every field whose value is null or an empty string.
  2. 其余字段按键名升序排列,拼成 k=v&k=v。值不做 URL 编码;布尔写 true / false。Sort the remaining keys ascending and join as k=v&k=v. No URL encoding; booleans are true / false.
  3. 用商户密钥做 HMAC-SHA256,取小写十六进制,放进 signature。HMAC-SHA256 with your secret, lowercase hex, sent as signature.

请求和通知用同一套规则。金额一律传字符串("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
currencyUSD(默认,目前只支持它)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
testtrue 为测试单,付款页带「测试单」标记;不传即真实订单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 to pay_url. Framing is not allowed.
  • 语言:中文为默认,加 ?lang=en 显示英文。Chinese by default; append ?lang=en for English.
  • 付款成功后 4 秒自动回到 redirect_url;订单过期时页面也提供返回按钮。不传则停在付款页。After payment the page returns to redirect_url within 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_noCrenel 订单标识与 8 位订单号Crenel order ID and 8-character reference
statuspaid
amount · currency订单金额(美元)Order amount (USD)
actual_amount实际到账,可能略高于或低于订单金额Amount received; may be slightly above or below the order amount
token · networkUSDT / USDC;网络键名见下表USDT / USDC; network keys are listed below
tx_hash链上交易哈希On-chain transaction hash
paid_at核实到账的时间,Unix 秒Time the payment was confirmed, Unix seconds
latetrue 表示过期后的宽限期内才到账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 as late included.
  • 同一张单可能收到不止一次通知,按 trade_id 去重。A notification can arrive more than once; de-duplicate on trade_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 becomes paid with late: 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 carries short.
  • 付清后又转来的钱、宽限期之后才到的钱不会自动入账,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 with test: 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_id reused 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

网络NetworknetworkUSDTUSDC订单金额Order amount
TRONtron●—> $500
BNB Chainbsc●●
Ethereumethereum●●> $50
Polygonpolygon●●
Arbitrum Onearbitrum●●
Basebase—●
OP Mainnetoptimism●●
Avalancheavalanche●●
X Layerxlayer●●
Arcarc—●
Plasmaplasma●—
Sonicsonic—●
Seisei—●
Monadmonad●●
Celocelo●●
Kaiakaia●—
Kavakava●—
XDCxdc—●
zkSync Erazksync—●
Berachainberachain●—
Unichainunichain●●
World Chainworldchain—●
Scrollscroll●—
Roninronin—●
Morphmorph●●
Stablestable●—
Tempotempo●—
Mantlemantle●●
HyperEVMhyperevm●—

金额门槛之下的订单,付款页不列出该网络。 Networks with a threshold are not offered for smaller orders.