ZH

商户 API 文档

Base URL: https://snapex.pro/api/v1/merchant

API 密钥与第一个请求

  1. 在后台获取密钥

    合作伙伴后台 → 开发者 → 「获取 API 密钥」。密钥格式为 smk_abcdefghij_ 后接 48 个字符,仅显示一次。如果丢失,请重新签发:旧密钥会立即失效。

  2. 添加一个请求头

    Authorization: Bearer your_key。

  3. 验证访问

    查询余额。如果返回包含 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 示例中的注释仅用于说明,真实响应中不包含。

收款

客户用 BTC 支付 60 USD 的订单

订单 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 时关闭订单:资金尚未入账。

客户用 USDT 支付

流程与 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 入账给您。

15 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 余额向外转账。

向客户发送价值 15 USDT 的 BTC

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。请重新请求预览。

向外部 TRON 钱包发送 USDT

区别在于 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 重新查询该对象。

配置

  1. 在后台设置 URL

    合作伙伴后台 → 开发者 → 「Webhook」卡片:填写 URL 并点击「保存」。每个账户只能设置一个 URL,它会接收下表中的所有事件类型。

  2. 保存签名密钥

    首次保存时,后台会显示一次 whsec_… 签名密钥。请只将其保存在您的服务器上。之后修改 URL,签名密钥保持不变。

  3. 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):同一事件可能多次到达,事件也可能乱序到达(重试的事件可能晚于更新的事件)。

签名验证

  1. 在解析 JSON 之前,以字节形式读取原始请求体。重新序列化后的 JSON 与签名不匹配。
  2. 以您的 whsec_… 签名密钥为密钥,对「{Webhook-Timestamp}.{原始请求体}」计算 HMAC-SHA256,并进行十六进制编码。
  3. 使用恒定时间比较函数(hash_equals、crypto.timingSafeEqual、hmac.compare_digest),将「v1=」+ 十六进制值与 Webhook-Signature 请求头进行比较。
  4. 拒绝与您服务器时钟相差超过 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);

处理程序检查清单

  • 基于原始请求体验证签名,并使用恒定时间比较;不匹配时返回 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