错误响应格式
API 会根据请求在哪一环失败,返回三种不同的错误信封。能处理全部三种的客户端,就永远不需要靠猜。应用错误
业务与应用错误(参数无效、资源缺失、冲突、速率限制、服务器错误)返回结构化信封:error_code(string):稳定的、机器可读的错误码(例如PAIR_NOT_FOUND、VALIDATION_ERROR、NOT_FOUND、RATE_LIMIT_EXCEEDED、INTERNAL_ERROR)。message(string):人类可读的错误描述。details(object):关于该错误的可选结构化上下文(可能为空)。request_id(string):请求关联标识符 —— 联系支持时请附上。
error_code 分支判断,不要基于 message。错误码是稳定的,文案不是。
请求校验错误(HTTP 422)
在进入应用逻辑之前就没通过 schema 校验的请求,会返回 FastAPI 的校验信封,HTTP 状态码为422:
detail(array<object>):每个校验失败项一条。loc(array<(string | integer)>):出错字段的路径。msg(string):校验错误信息。type(string):FastAPI/Pydantic 的校验错误类型。
遗留错误
少数接口端点的部分400 响应仍是一个裸的 detail 对象,不含 error_code 或 request_id —— 例如 POST /market/quote 上不受支持的路由,或 GET /market/pairs 上格式错误的 pair_ticker 过滤条件:
detail(string):人类可读的错误描述。
400 响应的客户端应同时兼容结构化信封和这种遗留结构。
HTTP 状态码
4xx 类表示请求本身需要修改;5xx 类表示请求没问题,是上游出了状况。只有 5xx 类加上 429 值得重试 —— 见重试与健壮性。
请求与认证
400 请求错误
400 请求错误
现象: 返回
400,响应体可能是 error_code 信封,也可能是一个裸的 detail 字符串原因: 请求中的参数缺失或无效。解决方法:- 核对负载,确认每个必填字段都存在且取值合法
- 让处理逻辑同时兼容两种
400结构 —— 有error_code就读它,否则回退到detail - 对于
{"detail": "Route not supported for this pair..."},用POST /api/v1/market/pairs/routes查询该交易对实际支持的routes,并发送其中出现过的from_asset.layer→to_asset.layer组合 - 对于交易对过滤,
pair_ticker必须是BASE/QUOTE格式(例如BTC/USDT),且单一交易对标识符之间互斥
401 未授权
401 未授权
现象: 原本正常的请求开始返回
401,或首次带密钥调用就返回 401原因:- 你所在环境已开启 API 密钥强制校验,而请求未携带有效密钥
- 请求头格式有误 —— 必须是
Authorization: Bearer <token>,而不是裸 token - 密钥缺少该接口端点所需的作用域(市场数据需要
quote:read,交换发起/执行需要swap:execute)
- 打印客户端实际发出的
Authorization请求头,检查是否漏了Bearer前缀或多了换行符 - 如果调用的是交换发起或执行,确认密钥带有
swap:execute而不只是quote:read - 确认密钥有效且未被吊销
- 如果你还没有密钥:signet 上匿名访问仍然可用 —— 收到
401说明强制校验对你已经生效
403 禁止访问
403 禁止访问
现象: 某个接口端点返回
403,而同一密钥在别处可用原因: 密钥本身有效,但不允许从此来源使用。解决方法: 换用允许的来源调用,或申请把该来源加入允许列表。当为后端签发的密钥被从浏览器或一台新的部署主机上使用时,就会遇到这种失败 —— 另见常见问题中关于 CORS 的说明。422 无法处理的实体
422 无法处理的实体
现象: 返回
422,且 detail 是一个数组而不是 error_code原因: 请求在进入业务逻辑之前就没通过 schema 校验,因此没有产生业务错误。解决方法: 读 detail[].loc —— 它就是出错字段的确切路径。["body", "rfq_id"] 配合 "type": "missing" 说明你完全没传 rfq_id;"type" 为 int_parsing 通常意味着金额被当作字符串发送了。不要把 422 当作可重试错误:同样的负载永远会失败。明明存在的资产、交易对或接口端点却返回 404
明明存在的资产、交易对或接口端点却返回 404
现象: 返回
PAIR_NOT_FOUND、NOT_FOUND,或你确知受支持的 ticker 返回了空的 assets 数组原因:- 接口端点 URL 或某个资源标识符有误
- 该资产或交易对处于非活跃状态,而
GET /market/pairs默认只返回活跃交易对(active_only=true) - 你用
asset_id过滤,但做市方要求完全一致的 RGB 合约 ID,包括rgb:前缀 - 你连到了错误的环境 —— 资产 ID 在 signet 和主网之间不通用
- 检查接口端点路径和资源标识符,包括
/api/v1前缀 - 用
active_only=false重新查询,看该交易对是否存在但被停用 - 从
GET /api/v1/market/assets动态发现 ID,不要硬编码,并按环境区分配置 - 检查响应里的
total—— 它是全部匹配项的数量,因此total大于你的分页大小说明你看到的是分页,而不是「不存在」
429 请求过多
429 请求过多
现象: 返回
RATE_LIMIT_EXCEEDED,通常发生在轮询价格时原因: 短时间内发送了过多请求。解决方法:- 读取
X-RateLimit-Remaining和X-RateLimit-Reset,在被切断之前就主动退避,而不是等429才反应 - 如果响应给出了等待时间,请在该时间之后重试
- 不要循环轮询
POST /market/quote—— 打开 WebSocket,在确实需要新价格时再发quote_request - 缓存
market/assets和market/pairs;两者都可安全缓存,且很少变化 - 记住限制是同时按 IP、按接口端点和按全局计算的,所以同一 NAT 后面一个吵闹的邻居可能会吃掉你的额度
500、502 或 503
500、502 或 503
现象: 返回
INTERNAL_ERROR,或者一分钟前还正常的接口端点返回 503原因:500—— 服务端出现意外错误502—— 从上游服务器收到了无效响应503—— 做市方或 LSP 节点暂时无法访问、正在维护或已过载
- 使用带抖动的指数退避重试;这类失败属于瞬时故障
- 超时后不要盲目重试
/swaps/execute—— 先轮询POST /api/v1/swaps/atomic/status,确认这笔调用是否其实已经生效 - 如果持续出现,请附上错误体中的
request_id反馈给我们
报价与金额
还没发起交换,报价就过期了
还没发起交换,报价就过期了
现象:
/swaps/init 拒绝了一个片刻之前还有效的 rfq_id原因: 报价的 expires_at 已过。这个窗口以数十秒计,不是数分钟。解决方法:- 尽可能晚地取报价 —— 紧贴发起之前,绝不要在用户可能久留的确认界面之前
- 让发起 → 白名单 → 执行整条链路保持紧凑;它们全都基于同一个报价
- 如果用户犹豫,丢弃这个
rfq_id,等他继续时再请求新报价
金额被判定为无效
金额被判定为无效
现象: 返回
VALIDATION_ERROR,或一个提到金额的 400原因:- 低于该层的
min_amount或高于max_amount—— 这些限制按层存放在每个资产的endpoints数组里,而不在资产本身上 - 发送了展示单位而不是原始单位
- 两侧都填了金额,或两侧都没填
- 从你所路由的那一层对应的
endpoints条目中读取限制 - BTC 一侧用毫聪,RGB 一侧用该资产
precision对应的原始单位 —— 见常见问题 from_asset.amount(正向报价)和to_asset.amount(反向报价)中恰好填一个
金额或价格差了几个数量级
金额或价格差了几个数量级
现象: 报价在算术上「不对」,差了 1,000 倍或 1 亿倍原因:
- BTC 一侧被当作聪处理,而 API 指的是毫聪
- 把一个资产的精度用在了另一个资产上 —— 精度是按资产定义的,BTC 和 USDT 并不共用一个
- 把
price当成了展示汇率;它是一个完整单位的from_asset以to_asset最小单位表示的价格
- 展示响应中的
to_asset.amount—— 手续费已经计入其中 —— 而不要用price反算 - 从
GET /api/v1/market/assets逐个资产读取precision,绝不硬编码换算系数 - 在把换算逻辑接入界面之前,先在 signet 上用一笔小额报价做一次往返验证
交换
发起成功,但 /swaps/execute 失败
发起成功,但 /swaps/execute 失败
现象:
init 返回了 swapstring,但 execute 失败原因: 这个 swapstring 从未在你自己的节点上加入白名单,因此接单方一侧无法履行 HTLC。这是最常见的集成故障,因为中间那一步并不是 Maker API 调用。解决方法: 三个步骤必须按顺序执行,且分布在两台不同的服务器上:- 在 Maker API 上调用
POST /api/v1/swaps/init→ 返回swapstring、payment_hash、access_token - 通过你自己的 RGB Lightning Node 的
/takerAPI 把swapstring加入白名单 - 在 Maker API 上调用
POST /api/v1/swaps/execute,带上swapstring、taker_pubkey和payment_hash
taker_pubkey 是你节点的 pubkey,且 payment_hash 与发起时返回的一致。参见交换协议。轮询状态时返回 404 Swap not found
轮询状态时返回 404 Swap not found
现象: 对一笔你确知存在的交换,
POST /api/v1/swaps/atomic/status 返回 404原因: access_token 缺失或无效。这个 404 是刻意统一的 —— 它不区分「token 错误」和「不存在这笔交换」,这样该接口就无法被用来探测 payment hash。解决方法:- 同时发送
payment_hash和/swaps/init返回的access_token - 如果 token 当时没有保存下来,它无法找回 —— 它只返回一次。请在发起时就把它和 payment hash 一起存好
- 确认你轮询的环境与这笔交换创建时所在的环境一致
交换卡在 Pending
交换卡在 Pending
现象:
execute 返回了 200,但状态一直是 Pending,资产也没到账原因:- HTLC 还在途中 ——
execute返回并不等于结算完成 - 资产一侧没有容量足够的路由
- 你节点上待处理的 RGB 转账尚未推进
- 继续轮询
/swaps/atomic/status;终态是Succeeded、Expired和Failed - 在你的节点上刷新转账,并检查该资产的出向容量 —— 不要只看总余额
- 用交换对象上的
expires_at与当前时间对比,判断 HTLC 还剩多少时间
交换以 Expired 或 Failed 结束
交换以 Expired 或 Failed 结束
现象: 状态变为
Expired 或 Failed原因:Expired—— 双方完成之前 HTLC 超时已过,通常是白名单或执行来得太晚Failed—— HTLC 无法路由或结算,通常是容量或流动性问题
- 你的资金没有风险:未完成的原子交换会自动把双方资金退回。不要手动重新打款
- 用新的报价重试 —— 旧的
rfq_id和swapstring已经作废 - 如果同一交易对反复失败,检查该资产通道上的容量,并通过 LSPS1 订购更多流动性
WebSocket
WebSocket 的错误不是 HTTP 响应 —— 它们以 JSON 消息的形式抵达已打开的连接;遇到严重错误时,服务器可能直接关闭连接。已连接,但收不到 quote_response
已连接,但收不到 quote_response
现象: 连接已打开、
quote_request 已发出,但没有 quote_response 返回原因: 请求失败了。失败会以带 error 字段的消息返回,而不是 quote_response,因此只监听 quote_response 的客户端看到的就是「一片安静」。解决方法:- 打印所有入站帧,而不只是你期待的那些,并处理
error结构 - 检查常见原因:未知资产、该交易对不支持的路由、超出按层限制的金额
- 确认
from_amount/to_amount中恰好设置了一个
价格不再更新
价格不再更新
现象: 第一条报价到了,之后就再没有变化原因: 这个协议是请求/响应式的 —— 没有订阅机制。服务器只回答你发出的那条消息,不会主动推送更新。解决方法: 每当你需要当前价格时就发一条新的
quote_request,由你自己的定时器或用户操作来驱动。连接反复断开
连接反复断开
现象: 连接意外关闭,有时发生在流程中途重连策略 —— 当连接意外关闭时:
- 等待几秒(5–10 秒)
- 重新建立连接
- 重新发送所有待处理的
quote_request以获取新报价 —— 报价不会跨越重连留存
- 定期发送
ping并期待pong;经过代理的空闲连接就是一条即将被关闭的连接 - 每个会话使用一个新的唯一
{client_id},并确认 URL 是wss://而不是ws:// - 如果你处在会剥离 WebSocket 升级的企业代理之后,请退回到
POST /market/quote
通道订单(LSPS1)
estimate_fees 或 create_order 提示缺少 rfq_id
estimate_fees 或 create_order 提示缺少 rfq_id
现象: 一旦带上
client_asset_amount,调用就失败原因: 当 client_asset_amount > 0 时,客户端是在购买资产,因此需要一个来自 POST /api/v1/market/quote 的新鲜 rfq_id 来定价。解决方法:- 先取报价,再把这个
rfq_id传给estimate_fees/create_order - 如果报价在两次调用之间过期了,重新取一次
- 如果你只想要 LSP 一侧的流动性,就完全不要传
client_asset_amount—— 那种情况下不需要报价
订单因通道大小或资产数量被拒
订单因通道大小或资产数量被拒
现象:
create_order 在余额或资产数量上校验失败原因: 请求超出了 LSP 公布的限制。解决方法:- 读取
GET /api/v1/lsps1/get_info返回的options,把参数控制在min_channel_balance_sat/max_channel_balance_sat、初始余额上下界以及max_channel_expiry_blocks之内 - 检查同一响应中按资产给出的上限 ——
min_initial_lsp_amount/max_initial_lsp_amount是按资产定义的,且可能为0,那表示该资产不支持这一侧 - 下单之前先连接
lsp_connection_url对应的 peer,这样通道才有地方开
订单停在 PENDING_RATE_DECISION
订单停在 PENDING_RATE_DECISION
现象: 通道始终没有开通,而
order_state 是 PENDING_RATE_DECISION原因: 付款结算之前市场汇率出现了明显变动,因此 LSP 暂停了订单,而不是按过时价格开通通道。它在等你决定。解决方法: 调用 POST /api/v1/lsps1/rate_decision,带上 order_id、该订单的 access_token 和 accept_new_rate —— true 表示按当前汇率继续,false 表示触发退款到 refund_onchain_address。退款会返回 refund_txid。对处于其他状态的订单调用该接口会返回 400。get_order 返回 400 或 404
get_order 返回 400 或 404
现象: 你无法读回自己创建的订单原因: 与交换不同,这两种情况是可区分的 ——
access_token 无效返回 400,order_id 不存在返回 404。解决方法: 把 create_order 返回的 access_token 和 order_id 一起保存;它只在创建时返回,而 get_order 和 rate_decision 都需要它。如果想要一层兜底提醒,可以在下单时设置 email。已付款,但通道还不能用
已付款,但通道还不能用
现象: 付款已结算,
order_state 为 CHANNEL_OPENING解决方法:- 继续轮询
get_order—— 它可以安全轮询,且在通道存在之前channel一直是null - 用
GET /api/v1/lsps1/network_info返回的当前高度与required_channel_confirmations对比 - 注意付款的过期时间在
payment.bolt11.expires_at和payment.onchain.expires_at下,通道自身的过期时间在channel.expires_at下 —— 顶层没有expires_at可读
重试与健壮性
把客户端设计成「坏响应是一种已处理的情况」,而不是一次崩溃。- 优雅地处理错误。 解析全部三种信封,基于
error_code分支判断,并给出可操作的提示,而不是把原始故障直接抛给用户。 - 只重试值得重试的。 瞬时故障 ——
500、502、503和429—— 值得再试一次,请使用带抖动的指数退避。客户端错误(400、401、403、404、422)无论发多少次,结果都一样。 - 尊重速率限制。 读取
X-RateLimit-*请求头,主动待在上限之内,而不是靠撞上限来发现它。缓存资产、交易对之类的静态数据。 - 把交换执行当作非幂等操作。
/swaps/execute超时并不能证明它失败了 —— 在重新发送任何东西之前,先轮询/swaps/atomic/status。 - 限制总重试次数。 面对
503的无限重试循环,会变成你对自己发起的拒绝服务。
反馈之前
一份可复现的报告通常一个来回就能定位。- 查看日志中完整的错误响应,而不只是状态码 —— 应用错误会带上
request_id,它把你的请求和我们的日志关联起来。 - 校验输入:确认每个参数和负载字段都符合该接口端点的要求,且金额使用的是原始单位。
- 用
curl复现,并隐去 API 密钥。 - 确认基础 URL 包含
/api/v1,并且指向你以为的那个环境。 - 试一下接口端点页面的交互式演练场(运行在 signet)—— 如果同样的调用在那里成功,差异就在你的客户端里。
获取帮助
如果是疑问而不是报错,请查看常见问题;规范与上游链接见更多资源。 其他情况请从下面的渠道中选择一个反馈问题,并附上:- 请求 URL 与 HTTP 方法
- 请求负载(请隐去 API 密钥)
- 完整的错误响应,包含
request_id - 请求的时间戳
- 运行环境(signet / 主网)
Telegram 社群
向社群提问。
邮件支持
紧急问题的直接支持渠道。