API 密钥与第一个请求
-
在后台获取密钥
合作伙伴后台 → 开发者 → 「获取 API 密钥」。密钥格式为 smk_abcdefghij_ 后接 48 个字符,仅显示一次。如果丢失,请重新签发:旧密钥会立即失效。
-
添加一个请求头
Authorization: Bearer your_key。
-
验证访问
查询余额。如果返回包含 data 的 JSON 响应,说明密钥已通过验证。
请求
curl -sS https://snapex.pro/api/v1/merchant/balances \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
响应
{
"data": [
{
"asset_id": "usdt", // 始终为 usdt,没有其他余额
"available_atomic": "15000000", // 15 USDT,以最小单位表示(6 位小数)
"available": "15", // 同一金额的十进制形式
"held_atomic": "0", // 为未完成的提现冻结
"held": "0"
}
]
}
如果请求失败
| 响应 |
原因 |
401 unauthorized |
密钥缺失、包含空格或被截断。Bearer 后面必须是完整的 smk_… 标识符。 |
401 key_inactive |
密钥已被吊销:已签发新密钥,但请求仍在使用旧密钥。 |
五条规则
大多数接入错误都源于违反了其中某一条规则。
1. 金额以字符串传递
传 amount(十进制字符串,如 "0.001"、"15")或 amount_atomic(以最小单位表示的整数,如 0.001 BTC 对应 "100000"),二者只能传一个。响应中始终同时返回两种形式:amount 与 amount_atomic 并列。
示例
"amount": "0.001" // 正确
"amount_atomic": "100000" // 同样正确
// 同时传两个 —— 422
2. asset_id 只取自 /assets
允许的代码:usdt、btc、xmr。任意值和区块浏览器中的 ID 都会被拒绝,返回 422。
示例
"asset_id": "btc" // 正确
"asset_id": "BTC" // 错误
"asset_id": "bitcoin" // 错误
3. 每个创建类请求都带 Idempotency-Key
幂等键是本次尝试的唯一标识。如果连接中断、结果未知,请使用同一个键重发请求:将返回原来的结果,不会再创建一笔操作。POST /invoices、POST /payments、POST /payouts/preview、POST /payouts 必须携带。
示例
Idempotency-Key: order-42-payment // 首次请求 → 201,已创建
Idempotency-Key: order-42-payment // 重试 → 200,返回同一资源
Idempotency-Key: order-42-payment // 金额不同 → 409 conflict
4. 空字段不返回
null 和空字符串不会出现在响应中。未支付的收款单没有 actual_amount、credited_amount 和 fee。请判断键是否存在,而不是判断它的值。
示例
// 未支付的收款单:
{ "payment_id": "pay_…", "status": "awaiting_deposit" }
// credited_amount 字段直接不存在
5. 不要在 expired 时关闭订单
expired 并不意味着转账永远不会到账。地址还会继续监控约 6 小时,迟到的转账也会入账。已入账的终态:confirmed、confirmed_underpaid、confirmed_overpaid。
场景
每个场景都覆盖完整流程:请求顺序、响应结构和字段含义。JSON 示例中的注释仅用于说明,真实响应中不包含。
收款
订单 order-42。您需要收到约 60 USDT;按 BTC 价格约 60,000 计算,客户应付约 0.001 BTC。
1
获取可用资产列表
响应中返回资产代码和 decimals,即小数位数。
请求
curl -sS https://snapex.pro/api/v1/merchant/assets \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
GET /assets
- 无需任何参数。
Authorization
- API 中唯一必需的请求头。
响应
{
"data": [
{
"asset_id": "btc", // ← 用于后续请求
"symbol": "BTC", // 用于向用户展示
"network": "bitcoin", // 用于提现
"decimals": 8, // 1 BTC = 100000000 个最小单位
"price_usd": "60000" // 参考价,不用于报价
},
{
"asset_id": "usdt",
"symbol": "USDT",
"network": "TRON",
"decimals": 6,
"price_usd": "1.00"
}
]
}
2
锁定汇率 10 分钟
在客户停留在支付页面期间锁定汇率。此步骤不会创建收款单或地址,只计算报价。响应为 0.001 BTC 将入账的 USDT 金额。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"asset_id":"btc","amount":"0.001"}'
要点
asset_id: "btc"
- 上一步返回的代码。
amount: "0.001"
- 客户应付的 BTC 金额。以字符串传递,不要传数字。
Idempotency-Key
- 可选:此步骤不会创建操作。传入后可复用同一报价。
响应
{
"data": {
"credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
// ← 保存,用于下一步
"asset_id": "btc",
"expected_amount": "0.001", // 客户应付金额
"expected_amount_atomic": "100000",
"credit_usdt": "59.4", // ← 将入账的金额
"credit_usdt_atomic": "59400000",
"expires_at": "2026-08-11T17:20:00+00:00"
// ← 超过此时间 credit-preview 失效
}
}
向客户展示 expected_amount,即需要转账的 BTC 数量。
3
创建收款单并获取地址
此步骤创建收款单并返回收款地址。请求中不要再传金额:金额已在第 2 步的 credit-preview 中确定。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: order-42-payment" \
-d '{
"credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"external_id": "order-42"
}'
要点
credit_lock_id
- credit-preview 响应中的 ID。如果锁定已过期,API 返回 422,请重新请求预览。
external_id: "order-42"
- 您的订单号。用于之后查找收款单,并避免同一订单产生两笔收款。
Idempotency-Key
- 必填。请使用稳定的唯一值,例如订单号加后缀。
响应
{
"data": {
"payment_id": "pay_01HZX...", // ← 用于查询状态的 ID
"external_id": "order-42", // 请求中的订单号
"asset_id": "btc",
"expected_amount": "0.001",
"credit_usdt": "59.4",
"status": "awaiting_deposit", // 等待转账
"channel": "api",
"deposit_address": "bc1qexampledepositaddress000000000",
// ← 展示给客户的地址
"expires_at": "2026-08-11T18:00:00+00:00",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
如果网络需要 memo,响应中会包含 deposit_memo。不带 memo 的转账不会入账,请同时展示这两个字段。
4
等待付款
每 10–30 秒轮询一次收款单。状态依次为 awaiting_deposit → detected → processing → confirmed。
请求
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
GET /payments/{payment_id}
- 使用上一步响应中的 payment_id。
触发情况
- 建议间隔 10–30 秒。频率限制为每个密钥每分钟 120 次请求,请勿更频繁地轮询。
响应
{
"data": {
"payment_id": "pay_01HZX...",
"status": "detected", // 已在网络上检测到转账,
// 确认数尚不足
"asset_id": "btc",
"expected_amount": "0.001",
"actual_amount": "0.001", // ← 实际到账金额
"deposit_address": "bc1qexampledepositaddress000000000",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
5
付款已确认
confirmed 表示资金已入账到余额,可以关闭订单。
请求
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
status: confirmed
- 终态,不会再变化。
credited_amount
- 实际入账的 USDT。足额付款时与 credit_usdt 一致。
响应
{
"data": {
"payment_id": "pay_01HZX...",
"external_id": "order-42",
"status": "confirmed", // ← 入账完成
"asset_id": "btc",
"expected_amount": "0.001",
"actual_amount": "0.001", // 应付金额已到账
"credit_usdt": "59.4",
"credited_amount": "59.4", // ← 入账到余额的金额
"fee": "0.6", // 手续费,已计入 credited_amount
"deposit_address": "bc1qexampledepositaddress000000000",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
对账请以 credited_amount 为准,而不是 expected_amount:付款不足时两者不同。
注意
- 不要提前创建收款单:汇率锁定仅持续 10 分钟,且地址是针对特定金额生成的。
- 不要在 detected 时关闭订单:资金尚未入账。
流程与 BTC 场景相同,只有 asset_id 不同。下面是简化示例。
1
锁定金额
无需兑换,因此 credit_usdt 约等于金额减去手续费。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-d '{"asset_id":"usdt","amount":"100"}'
要点
asset_id: "usdt"
- 与 BTC 场景唯一的区别。
amount: "100"
- 客户通过 TRON 网络转账 100 USDT。
响应
{
"data": {
"credit_lock_id": "b2c3d4e5-...",
"asset_id": "usdt",
"expected_amount": "100",
"credit_usdt": "99", // ← 手续费 1 USDT
"expires_at": "2026-08-11T17:20:00+00:00"
}
}
2
创建收款单
与前面的步骤相同。地址为 TRON 地址(以 T 开头)。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-43-payment" \
-d '{"credit_lock_id":"b2c3d4e5-...","external_id":"order-43"}'
要点
Idempotency-Key
- 新订单请使用新的 Idempotency-Key,不要复用 order-42 的键。
响应
{
"data": {
"payment_id": "pay_01HZY...",
"external_id": "order-43",
"asset_id": "usdt",
"expected_amount": "100",
"credit_usdt": "99",
"status": "awaiting_deposit",
"deposit_address": "TXYZexampletronaddress000000000",
// ← TRON 地址,TRC20
"expires_at": "2026-08-11T18:00:00+00:00"
}
}
请告知客户网络为 TRC20。通过其他网络转入此地址的 USDT 不会入账。
应付 0.001 BTC,实际到账 0.0009 BTC,例如客户钱包从金额中扣除了网络手续费。
1
查询收款单
如果到账金额低于 expected_amount,但仍足以入账,状态为 confirmed_underpaid,credited_amount 低于报价。如果金额过小,则不会入账。
请求
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
含义
- 请比较 credited_amount,而不是 credit_usdt。前者是实际入账金额,后者是足额付款时的报价。
响应
{
"data": {
"payment_id": "pay_01HZX...",
"external_id": "order-42",
"status": "confirmed_underpaid", // ← 付款不足,但资金已入账
"asset_id": "btc",
"expected_amount": "0.001", // 应付金额
"actual_amount": "0.0009", // 实际到账金额
"credit_usdt": "59.4", // 足额付款时的报价
"credited_amount": "53.4", // ← 实际入账金额
"fee": "0.6",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
后续处理由您决定:要求客户补款、部分履约或取消订单。API 返回实际入账的金额。
注意
- 不要通过比较 actual_amount 与 expected_amount 来判断「是否已支付」。请将 credited_amount 与您需要的 USDT 金额进行比较。
expires_at 已过且没有转账到账。收款单变为 expired,地址监控仍在继续。
1
状态 expired
付款时间窗口已关闭。地址还会继续监控约 6 小时。
请求
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
status: expired
- 付款时间窗口已到期。
credited_amount
- 尚无资金到账。
响应
{
"data": {
"payment_id": "pay_01HZX...",
"status": "expired", // 时间窗口已到期
"asset_id": "btc",
"expected_amount": "0.001",
"deposit_address": "bc1qexampledepositaddress000000000",
"expires_at": "2026-08-11T18:00:00+00:00"
// 没有 actual_amount 和 credited_amount:转账未到账
}
}
2
客户逾期付款
如果转账在额外监控期内到账,收款单仍会变为 confirmed。不要在 expired 时删除订单:将其标记为逾期并继续轮询。
请求
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
expired → confirmed
- 正常情况:转账在 expires_at 之后到账。
响应
{
"data": {
"payment_id": "pay_01HZX...",
"status": "confirmed", // ← 在 expires_at 之后入账
"actual_amount": "0.001",
"credited_amount": "59.4",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
注意
- expired 后不要立即生成新地址:客户可能正在向旧地址转账。我们还会继续等待该转账 6 小时。
账单
以 USD 计价的账单,附托管支付页面。客户自选网络,款项以 USDT 入账给您。
订单 order-15。商户创建 USD 账单,并将支付页面链接发送给客户。
1
创建账单
账单锁定 USD 金额和手续费承担方,并返回 pay_url。此时尚未生成地址。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: order-15-invoice" \
-d '{
"amount_usd": "15.00",
"fee_on": "merchant",
"external_id": "order-15"
}'
要点
amount_usd
- 订单金额,以字符串传递。此接口只接受 USD,不接受 amount_atomic。
fee_on
- merchant:手续费由您承担,客户看到的金额为 15.00。customer:手续费由客户承担。
Idempotency-Key
- 必填。使用同一个键重试会返回同一张账单。
partner_ref
- 可选。为您完成接入的集成商的 Partner ID(SnapEX Pay 推荐计划)。它会将您的账户一次性绑定到该合作伙伴;如果您自行接入,则无需传入。
响应
{
"data": {
"invoice_id": "INV-8K2M1Q0P",
"amount_usd": "15.00",
"fee_on": "merchant",
"charge_usd": "15.00",
"credit_usd": "13.85",
"status": "open",
"pay_url": "https://pay.snapex.pro/0123456789abcdef0123456789abcdef",
"methods": [
{ "asset_id": "usdt.trc20", "symbol": "USDT", "network": "TRC20" },
{ "asset_id": "usdt.erc20", "symbol": "USDT", "network": "ERC20" },
{ "asset_id": "usdt.bep20", "symbol": "USDT", "network": "BEP20" }
]
}
}
密钥需要 invoices.write 或 payments.write 权限。测试环境:environment=test。
2
客户选择网络
此步骤生成唯一地址。在转账到账前,可以更换支付方式。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P/methods \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-15-trc20" \
-d '{"asset_id":"usdt.trc20"}'
要点
asset_id: "usdt.trc20"
- 取自 methods 的代码:usdt.trc20、usdt.erc20 或 usdt.bep20。其他币种也来自同一列表。
payment.address
- 只展示此地址和金额。不使用 Memo 和 Tag。
响应
{
"data": {
"invoice_id": "INV-8K2M1Q0P",
"status": "awaiting",
"selected_asset_id": "usdt.trc20",
"payment": {
"asset_id": "usdt.trc20",
"amount": "15 USDT",
"address": "TExampleUniqueAddressForThisAttempt",
"status": "awaiting"
}
}
}
可以跳过此调用:客户可在 pay_url 页面上选择网络。
3
账单已支付
第一笔完成的转账会关闭账单。之后转入其他地址的款项会单独入账。
请求
curl -sS https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
status: paid
- 终态。可以关闭订单。
invoice.paid
- invoice.paid Webhook 会推送到后台中设置的 URL。签名验证见「Webhook」章节。
响应
{
"data": {
"invoice_id": "INV-8K2M1Q0P",
"status": "paid",
"credit_usd": "13.85",
"paid_at": "2026-09-25T12:00:10+00:00"
}
}
付款不足不会关闭账单:状态为 underpaid,同一地址等待补足余款(USDT 为 72 小时)。如果余款未按时到账,账单最终为 partially_paid。
注意
- 不要在面向客户的界面中提及内部路由。支付页面只显示地址、金额和网络。
- 没有退款 API。第二笔完成的转账是单独入账,不是对第一笔的冲正。
提现
从您的 USDT 余额向外转账。
amount 是从您余额中扣除的金额,而不是收款方收到的金额。收款方到账金额由预览接口返回。
1
查询可用余额
请使用 available,而不是 available 加 held。held 为未完成的提现冻结,不能使用。
请求
curl -sS https://snapex.pro/api/v1/merchant/balances \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
available
- 可扣除的可用余额。
held
- 为处理中的提现冻结。
响应
{
"data": [
{
"asset_id": "usdt",
"available": "74.4", // ← 可用金额
"available_atomic": "74400000",
"held": "0", // 无冻结
"held_atomic": "0"
}
]
}
2
获取提现报价
预览返回 15 USDT 对应收款方可收到的 BTC 数量及手续费。此时尚未扣款,只锁定报价。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: wd-77-preview" \
-d '{
"destination_asset_id": "btc",
"destination_network": "bitcoin",
"destination_address": "bc1qexampledestinationaddress000000",
"amount": "15"
}'
要点
amount: "15"
- 从您余额中扣除的 USDT,不是收款方到账金额。
destination_asset_id
- 收款方资产。取自 GET /assets 的代码。
destination_network
- 收款方网络。BTC 为 bitcoin。取自 GET /assets 中的 network。
destination_address
- 收款地址。发送前请核对:转账无法撤销。
Idempotency-Key
- 必填。预览请使用单独的键。
响应
{
"data": {
"preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
// ← 用于下一步的 ID
"from_asset_id": "usdt",
"from_amount": "15", // 扣除金额
"to_asset_id": "btc",
"to_amount": "0.000221", // ← 收款方到账金额
"fee_amount": "0.000019", // 以 BTC 计的手续费,已包含在内
"expires_at": "2026-08-11T17:20:00+00:00"
// ← 须在此时间前执行
}
}
将 to_amount 作为用户将收到的金额展示。如果金额不可接受,不要执行该预览:它会在 expires_at 失效。
3
执行提现
此步骤扣款并发送。请在请求中再次传入地址和网络,以防两步之间被篡改。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: wd-77-execute" \
-d '{
"preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
"destination_network": "bitcoin",
"destination_address": "bc1qexampledestinationaddress000000",
"external_id": "wd-77"
}'
要点
Idempotency-Key: wd-77-execute
- 新的键,不能与预览的键相同。复用预览的键会返回 409。
destination_network / address
- 必须与预览完全一致,否则返回 422。
external_id
- 您的操作 ID,便于之后查询。
响应
{
"data": {
"payout_id": "payout_01HZX...", // ← 用于跟踪的 ID
"external_id": "wd-77",
"asset_id": "usdt",
"debit_amount": "15", // 已从余额扣除
"destination_asset_id": "btc",
"destination_network": "bitcoin",
"destination_address": "bc1qexampledestinationaddress000000",
"destination_amount": "0.000221",// 收款方到账金额
"fee": "0.000019",
"status": "reserved", // 资金已冻结,开始发送
"channel": "api",
"created_at": "2026-08-11T17:10:00+00:00"
}
}
4
等待完成
状态依次为 reserved → submitting → processing → completed。每 15–30 秒轮询一次。
请求
curl -sS https://snapex.pro/api/v1/merchant/payouts/payout_01HZX... \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
status: completed
- 资金已送达。终态。
status: failed
- 转账失败;冻结金额通常会退回余额。
status: manual_review
- 结果不确定。请等待审核,不要再创建一笔提现。
响应
{
"data": {
"payout_id": "payout_01HZX...",
"external_id": "wd-77",
"debit_amount": "15",
"destination_amount": "0.000221",
"status": "completed", // ← 已送达
"created_at": "2026-08-11T17:10:00+00:00"
}
}
注意
- amount 是扣除金额,不是收款方到账金额。
- 提现有单独的频率限制:每分钟 5 次、每小时 30 次请求。不要循环调用预览。
- 如果预览已过期(expires_at),POST /payouts 返回 422。请重新请求预览。
区别在于 destination_asset_id 也是 usdt。此时手续费最低。
1
获取提现报价
扣款资产与到账资产相同,因此 to_amount 约等于 amount 减去网络手续费。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-78-preview" \
-d '{
"destination_asset_id": "usdt",
"destination_network": "TRON",
"destination_address": "TXYZexampletronaddress000000000",
"amount": "50"
}'
要点
destination_network: "TRON"
- 取自 GET /assets 中的 network,区分大小写。
amount: "50"
- 将扣除 50 USDT。
响应
{
"data": {
"preview_id": "d4e5f6a7-...",
"from_asset_id": "usdt",
"from_amount": "50", // 扣除金额
"to_asset_id": "usdt",
"to_amount": "48.9", // ← 到账金额(已扣网络手续费)
"fee_amount": "1.1",
"expires_at": "2026-08-11T17:25:00+00:00"
}
}
2
执行提现
步骤与上一个场景相同。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-78-execute" \
-d '{
"preview_id": "d4e5f6a7-...",
"destination_network": "TRON",
"destination_address": "TXYZexampletronaddress000000000",
"external_id": "wd-78"
}'
响应
{
"data": {
"payout_id": "payout_01HZZ...",
"debit_amount": "50",
"destination_asset_id": "usdt",
"destination_amount": "48.9",
"status": "reserved",
"created_at": "2026-08-11T17:26:00+00:00"
}
}
注意
- 请确认收款地址属于 TRON 网络。转往其他网络的转账不会执行:请求会被拒绝,或资金无法到账。
余额为 10 USDT,请求提现 15 USDT。
1
预览拒绝请求
余额在报价时检查,早于任何扣款。可以立即向用户展示该错误。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-79-preview" \
-d '{
"destination_asset_id": "btc",
"destination_network": "bitcoin",
"destination_address": "bc1q…",
"amount": "15"
}'
要点
HTTP 422
- 业务拒绝。返回格式与校验错误相同。
details.amount
- 错误绑定到字段,可直接显示在表单上。
响应
{
"error": {
"code": "validation_error",
"message": "The request is invalid.",
"details": {
"amount": ["余额不足。"]
// ← 拒绝原因绑定到 amount
}
}
}
不会冻结任何资金。使用同一个 Idempotency-Key 重试是安全的,但请先充值余额。
注意
- 请考虑 held:如果部分资金已为另一笔提现冻结,available 会低于预期。
辅助操作
账本(ledger)即操作日志:入账、扣款和冻结分别记为独立的记录。
1
分页获取记录
记录按时间倒序返回。每次请求最多 100 条,下一页使用 cursor 获取。
请求
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50" \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
limit=50
- 每次响应的记录数:1 至 100,默认 25。
status=payment
- 可选:按记录类型筛选。
响应
{
"data": [
{
"id": "124",
"direction": "credit", // 入账
"type": "payment", // 来源:收款单
"amount": "59.4",
"asset_id": "usdt",
"channel": "api",
"occurred_at": "2026-08-11T17:05:00+00:00"
},
{
"id": "125",
"direction": "reserve", // 为提现冻结
"type": "payout",
"amount": "15",
"asset_id": "usdt",
"occurred_at": "2026-08-11T17:10:00+00:00"
}
],
"meta": {
"next_cursor": "eyJpZCI6MTI1fQ", // ← cursor 参数的值
"per_page": 50
}
}
2
下一页
当 next_cursor 为 null 时,表示没有更多记录。
请求
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50&cursor=eyJpZCI6MTI1fQ" \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Accept: application/json"
要点
cursor
- 上一次响应中 meta.next_cursor 的值。不接受任意值。
响应
{
"data": [ /* 还有更多记录 */ ],
"meta": {
"next_cursor": null, // ← 最后一页
"per_page": 50
}
}
注意
- direction 为 reserve 和 release 表示冻结和解冻,不是向外转账。请勿将其计为支出。
1
重发同一请求
使用同一个 Idempotency-Key 和相同参数。服务器会将其视为同一笔操作。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-77-execute" \
-d '{
"preview_id": "c3d4e5f6-...",
"destination_network": "bitcoin",
"destination_address": "bc1qexampledestinationaddress000000",
"external_id": "wd-77"
}'
要点
Idempotency-Key
- 新的键会创建一笔新的提现。
HTTP 200 / 201
- 操作已存在,返回原对象。
响应
{
"data": {
"payout_id": "payout_01HZX...", // ← 与首次响应相同的 ID
"external_id": "wd-77",
"debit_amount": "15",
"status": "processing", // 等待期间状态可能已变化
"created_at": "2026-08-11T17:10:00+00:00"
}
}
HTTP 201 表示本次请求创建了该操作,HTTP 200 表示该操作已存在。两者均表示成功。
2
如果参数已更改
使用同一个键但金额或地址不同的请求会被拒绝,以防参数不一致。
请求
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
-H "Authorization: Bearer smk_abcdefghij_yourkey" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wd-77-execute" \
-d '{
"preview_id": "another-preview",
"destination_network": "bitcoin",
"destination_address": "bc1qanotheraddress"
}'
要点
HTTP 409
- 冲突:该键已绑定到另一笔操作。
处理方式
- 如果是新的提现,请使用新的 Idempotency-Key。
响应
{
"error": {
"code": "conflict",
"message": "Idempotency-Key was used with different parameters."
}
}
注意
- 幂等键只生成一次,并与订单一起保存。每次发送前都生成新键,会使防重复保护失效。
- 收到 HTTP 429 时,请在几秒后使用同一个键重试。
Webhook
当账单、收款单、提现、兑换或余额发生变化时,SnapEX 会向您的 URL 发送 POST 请求。Webhook 只是通知,不是权威数据来源:请验证签名,涉及重要决策时,请通过 API 重新查询该对象。
配置
-
在后台设置 URL
合作伙伴后台 → 开发者 → 「Webhook」卡片:填写 URL 并点击「保存」。每个账户只能设置一个 URL,它会接收下表中的所有事件类型。
-
保存签名密钥
首次保存时,后台会显示一次 whsec_… 签名密钥。请只将其保存在您的服务器上。之后修改 URL,签名密钥保持不变。
-
URL 要求
使用 https:// 开头的绝对 URL,且主机为公网主机:URL 中不得包含 login:password,不得使用 localhost,主机也不得解析到私有或保留 IP。URL 在保存时校验,每次推送前也会再次校验。
推送内容
| 请求头 |
含义 |
Webhook-Id |
事件唯一 ID(UUID),与请求体中的 id 相同。重试或手动重发时不变,请据此去重。 |
Webhook-Timestamp |
本次推送发出时的 Unix 时间(秒)。每次重试都会带上新的值和新的签名。 |
Webhook-Signature |
v1= 后接字符串「{Webhook-Timestamp}.{原始请求体}」的 HMAC-SHA256 小写十六进制值。密钥为完整的签名密钥,包含 whsec_ 前缀。 |
Content-Type |
始终为 application/json。请求体为 UTF-8 编码的 JSON。 |
请求
POST /hooks/snapex HTTP/1.1
Content-Type: application/json
Webhook-Id: 6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f
Webhook-Timestamp: 1790337610
Webhook-Signature: v1=2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
请求体
{
"id": "6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f", // = Webhook-Id
"type": "invoice.paid", // 事件类型,见下表
"created_at": "2026-09-25T12:00:10+00:00", // 事件发生时间
"data": { // 事件发生时的对象
"invoice_id": "INV-8K2M1Q0P",
"environment": "live",
"amount_usd": "15.00",
"charge_usd": "15.00",
"credit_usd": "13.85",
"status": "paid",
"external_id": "order-15",
"paid_at": "2026-09-25T12:00:10+00:00"
}
}
data 的内容取决于事件类型:invoice.* 为账单,结构与 GET /invoices/{id} 相同(attempts 为空,当前支付尝试在 payment 中);payment.* 为收款单(payment_id、external_id、status、金额);payout.* 为提现(payout_id、external_id、status、金额);conversion.* 为兑换(conversion_id、status、金额);balance.changed 为变动后的 asset_id、entry_type、available_atomic 和 held_atomic。
事件类型
| 事件 |
含义 |
invoice.created |
账单已创建,尚未选择支付方式(open)。 |
invoice.awaiting |
已选择支付方式并生成地址(awaiting)。 |
invoice.underpaid |
部分金额已到账,同一地址等待补足余款(underpaid)。 |
invoice.paid |
已全额支付并入账(paid,终态)。这是发货的唯一依据。 |
invoice.partially_paid |
客户仅支付了部分金额,补款期已结束,已收到的部分已入账给您(partially_paid,终态)。 |
invoice.expired |
账单有效期结束,未收到付款(expired,终态)。 |
invoice.extra_payment |
账单支付后又收到一笔转账,该笔款项单独入账。 |
payment.created |
收款单已创建。 |
payment.detected |
已在网络上检测到转账,但尚未入账。 |
payment.credited |
收款单已入账(confirmed* 状态之一)。 |
payment.expired |
收款单时间窗口已到期。 |
payout.created |
提现已创建,资金已冻结。 |
payout.processing |
提现正在发送。 |
payout.completed |
资金已送达收款方。 |
payout.failed |
提现失败。 |
conversion.completed |
兑换已完成。 |
conversion.failed |
兑换全部或部分失败。 |
balance.changed |
已记录一笔余额变动:入账、冻结或扣款。 |
仅在收到 invoice.paid 时发货:只有全额支付后才会发送该事件。invoice.underpaid 和 invoice.partially_paid 表示金额不足。
使用 environment=test 创建的账单为模拟账单:它们经历相同的状态并发送相同的 Webhook(data.environment = "test"),但绝不会改变真实余额。
推送与重试
- 任何 2xx 响应都视为成功。其他状态码、超时或网络错误均视为失败。不解析响应体。
- 超时:连接 5 秒,整个请求 15 秒。
- 不跟随重定向:301/302 视为失败。请设置最终 URL。
- 每个事件最多推送 5 次:首次推送之后,每次失败后分别在约 10 秒、1 分钟、5 分钟和 15 分钟后重试。第五次失败后停止自动推送。
- 「Webhook 推送记录」卡片中列出事件、HTTP 状态码或错误、时间和尝试次数。点击「重新发送」按钮,会使用同一个 Webhook-Id 再次发送该事件。
- 推送保证至少一次(at-least-once):同一事件可能多次到达,事件也可能乱序到达(重试的事件可能晚于更新的事件)。
签名验证
- 在解析 JSON 之前,以字节形式读取原始请求体。重新序列化后的 JSON 与签名不匹配。
- 以您的 whsec_… 签名密钥为密钥,对「{Webhook-Timestamp}.{原始请求体}」计算 HMAC-SHA256,并进行十六进制编码。
- 使用恒定时间比较函数(hash_equals、crypto.timingSafeEqual、hmac.compare_digest),将「v1=」+ 十六进制值与 Webhook-Signature 请求头进行比较。
- 拒绝与您服务器时钟相差超过 5 分钟的 Webhook-Timestamp(建议)。重试会带上新的时间戳,因此不受影响。
PHP
<?php
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // 后台中的 whsec_…
$body = file_get_contents('php://input'); // 原始请求体,解析 JSON 之前
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! ctype_digit($timestamp)
|| abs(time() - (int) $timestamp) > 300
|| ! hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// 如果该 Webhook-Id 已处理过则跳过;耗时操作放入队列处理
http_response_code(200);
Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.SNAPEX_WEBHOOK_SECRET; // 后台中的 whsec_…
// 原始请求体,解析 JSON 之前
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('Webhook-Timestamp') || '';
const signature = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret)
.update(timestamp + '.').update(req.body).digest('hex'));
const ok = /^\d+$/.test(timestamp)
&& Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300
&& signature.length === expected.length
&& crypto.timingSafeEqual(signature, expected);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// 如果该 Webhook-Id 已处理过则跳过;耗时操作放入队列处理
res.sendStatus(200);
});
Python
import hashlib, hmac, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["SNAPEX_WEBHOOK_SECRET"].encode() # 后台中的 whsec_…
@app.post("/hooks/snapex")
def snapex_webhook():
body = request.get_data() # 原始请求体,解析 JSON 之前
timestamp = request.headers.get("Webhook-Timestamp", "")
signature = request.headers.get("Webhook-Signature", "")
expected = "v1=" + hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
if not (timestamp.isascii() and timestamp.isdigit()) \
or abs(time.time() - int(timestamp)) > 300 \
or not hmac.compare_digest(expected.encode(), signature.encode()):
abort(400)
event = request.get_json()
# 如果该 Webhook-Id 已处理过则跳过;耗时操作放入队列处理
return "", 200
处理程序检查清单
- 基于原始请求体验证签名,并使用恒定时间比较;不匹配时返回 400。
- 拒绝 Webhook-Timestamp 距今超过 5 分钟的请求。
- 保存已处理的 Webhook-Id,忽略重复事件。不要依赖顺序:将事件中的状态与您已有的状态进行比较。
- 尽快返回 2xx(远低于 15 秒),耗时操作异步处理。
- 收到 Webhook 后,可以通过 API 重新查询对象进行确认:GET /invoices/{id}、GET /payments/{id}、GET /payouts/{id}。
状态
带「终态」标记的状态不会再变化。唯一的例外是收款单的 expired 状态:迟到的转账仍可能到账。
收款单
| 状态 |
含义 |
处理方式 |
|
awaiting_deposit |
地址已生成,等待转账 |
向客户展示地址和金额 |
|
detected |
已在网络上检测到转账,尚未入账 |
等待,不要关闭订单 |
|
processing |
正在兑换为 USDT |
等待 |
|
confirmed |
应付金额已到账,资金已入账 |
关闭订单 |
终态 |
confirmed_underpaid |
到账金额低于应付金额,在允许范围内 |
查看 credited_amount,判断是否足够 |
终态 |
confirmed_overpaid |
到账金额高于应付金额,按 credit-preview 入账 |
关闭订单。超出部分请联系客服 |
终态 |
confirmed_mismatch |
为兼容性保留 |
新收款单不会出现此状态 |
终态 |
manual_review |
需要人工审核 |
等待审核,不要再创建一笔收款单 |
|
expired |
付款时间窗口已到期,地址还会继续监控约 6 小时 |
不要删除订单,继续轮询 |
|
failed |
服务商侧故障 |
创建新的收款单 |
终态 |
cancelled |
收款单已取消 |
创建新的收款单 |
终态 |
refunded |
已由服务商退款 |
核对余额;如不一致请联系客服 |
终态 |
账单
| 状态 |
含义 |
处理方式 |
|
open |
账单已创建,尚未选择支付方式 |
将 pay_url 提供给客户,或自行选择 asset_id |
|
awaiting |
地址已生成,等待转账 |
展示地址和金额;轮询状态 |
|
underpaid |
部分金额已到账,地址不变 |
请客户将余款转入此地址 |
|
paid |
第一笔足额转账已入账 |
关闭订单 |
终态 |
partially_paid |
客户仅支付了部分金额,补款期已结束,已收到的部分已入账给您 |
不要按已支付订单发货;与客户协商处理余款 |
终态 |
expired |
账单有效期已结束 |
创建新的账单 |
终态 |
提现
| 状态 |
含义 |
处理方式 |
|
reserved |
已为提现冻结资金 |
等待 |
|
submitting |
已提交给服务商 |
等待 |
|
processing |
服务商正在链上发送 |
等待 |
|
completed |
资金已送达收款方 |
完成 |
终态 |
manual_review |
结果不确定 |
不要再创建一笔提现;等待审核 |
|
failed |
转账失败;冻结金额通常会退回 |
查看余额;如有需要,使用新的 Idempotency-Key 重试 |
终态 |
错误
成功响应始终在 data 中,错误始终在 error 中,没有其他返回格式。
错误结构
{
"error": {
"code": "validation_error", // 机器可读的错误码,见下表
"message": "The request is invalid.",
"details": { // 并非总是返回
"destination_address": ["The destination address field is required."]
// 键名为未通过校验的字段
}
}
}
| HTTP |
code |
触发情况 |
处理方式 |
| 401 |
unauthorized |
密钥缺失或无效 |
检查 Authorization: Bearer smk_… 请求头是否完整,不含空格或换行 |
| 401 |
key_inactive |
密钥已吊销 |
旧密钥已被吊销。请使用后台中的当前密钥 |
| 401 |
key_expired |
密钥已过期 |
签发新密钥 |
| 404 |
not_found |
ID 不存在或属于其他商户 |
使用您自己的响应中返回的 payment_id 或 payout_id |
| 409 |
conflict |
Idempotency-Key 已绑定到另一笔操作,或 external_id 已被使用 |
新操作请使用新的键。重试请使用同一个键和相同参数 |
| 422 |
validation_error |
字段无效、credit-preview 已过期、余额不足或缺少 Idempotency-Key |
查看 details 中的字段和原因 |
| 429 |
rate_limited |
超出频率限制 |
稍后使用同一个 Idempotency-Key 重试 |
| 500 |
internal_error |
服务器内部错误 |
稍后使用同一个 Idempotency-Key 重试。如果问题持续,请联系客服并提供 X-Request-Id |