Skip to main content
KaleidoSwap API 使用标准 HTTP 状态码和结构化错误响应体来报告请求结果。本页说明如何读懂一个错误、每个状态码的含义,以及集成方实际会遇到的那些故障该怎么修。如果是疑问而不是故障,请看常见问题

错误响应格式

API 会根据请求在哪一环失败,返回三种不同的错误信封。能处理全部三种的客户端,就永远不需要靠猜。

应用错误

业务与应用错误(参数无效、资源缺失、冲突、速率限制、服务器错误)返回结构化信封:
  • error_codestring):稳定的、机器可读的错误码(例如 PAIR_NOT_FOUNDVALIDATION_ERRORNOT_FOUNDRATE_LIMIT_EXCEEDEDINTERNAL_ERROR)。
  • messagestring):人类可读的错误描述。
  • detailsobject):关于该错误的可选结构化上下文(可能为空)。
  • request_idstring):请求关联标识符 —— 联系支持时请附上。
请基于 error_code 分支判断,不要基于 message。错误码是稳定的,文案不是。

请求校验错误(HTTP 422)

在进入应用逻辑之前就没通过 schema 校验的请求,会返回 FastAPI 的校验信封,HTTP 状态码为 422
  • detailarray<object>):每个校验失败项一条。
    • locarray<(string | integer)>):出错字段的路径。
    • msgstring):校验错误信息。
    • typestring):FastAPI/Pydantic 的校验错误类型。

遗留错误

少数接口端点的部分 400 响应仍是一个裸的 detail 对象,不含 error_coderequest_id —— 例如 POST /market/quote 上不受支持的路由,或 GET /market/pairs 上格式错误的 pair_ticker 过滤条件:
  • detailstring):人类可读的错误描述。
处理 400 响应的客户端应同时兼容结构化信封和这种遗留结构。

HTTP 状态码

4xx 类表示请求本身需要修改;5xx 类表示请求没问题,是上游出了状况。只有 5xx 类加上 429 值得重试 —— 见重试与健壮性

请求与认证

现象: 返回 400,响应体可能是 error_code 信封,也可能是一个裸的 detail 字符串原因: 请求中的参数缺失或无效。解决方法:
  1. 核对负载,确认每个必填字段都存在且取值合法
  2. 让处理逻辑同时兼容两种 400 结构 —— 有 error_code 就读它,否则回退到 detail
  3. 对于 {"detail": "Route not supported for this pair..."},用 POST /api/v1/market/pairs/routes 查询该交易对实际支持的 routes,并发送其中出现过的 from_asset.layerto_asset.layer 组合
  4. 对于交易对过滤,pair_ticker 必须是 BASE/QUOTE 格式(例如 BTC/USDT),且单一交易对标识符之间互斥
现象: 原本正常的请求开始返回 401,或首次带密钥调用就返回 401原因:
  • 你所在环境已开启 API 密钥强制校验,而请求未携带有效密钥
  • 请求头格式有误 —— 必须是 Authorization: Bearer <token>,而不是裸 token
  • 密钥缺少该接口端点所需的作用域(市场数据需要 quote:read,交换发起/执行需要 swap:execute
解决方法:
  1. 打印客户端实际发出的 Authorization 请求头,检查是否漏了 Bearer 前缀或多了换行符
  2. 如果调用的是交换发起或执行,确认密钥带有 swap:execute 而不只是 quote:read
  3. 确认密钥有效且未被吊销
  4. 如果你还没有密钥:signet 上匿名访问仍然可用 —— 收到 401 说明强制校验对你已经生效
现象: 某个接口端点返回 403,而同一密钥在别处可用原因: 密钥本身有效,但不允许从此来源使用。解决方法: 换用允许的来源调用,或申请把该来源加入允许列表。当为后端签发的密钥被从浏览器或一台新的部署主机上使用时,就会遇到这种失败 —— 另见常见问题中关于 CORS 的说明。
现象: 返回 422,且 detail 是一个数组而不是 error_code原因: 请求在进入业务逻辑之前就没通过 schema 校验,因此没有产生业务错误。解决方法:detail[].loc —— 它就是出错字段的确切路径。["body", "rfq_id"] 配合 "type": "missing" 说明你完全没传 rfq_id"type"int_parsing 通常意味着金额被当作字符串发送了。不要把 422 当作可重试错误:同样的负载永远会失败。
现象: 返回 PAIR_NOT_FOUNDNOT_FOUND,或你确知受支持的 ticker 返回了空的 assets 数组原因:
  • 接口端点 URL 或某个资源标识符有误
  • 该资产或交易对处于非活跃状态,而 GET /market/pairs 默认只返回活跃交易对(active_only=true
  • 你用 asset_id 过滤,但做市方要求完全一致的 RGB 合约 ID,包括 rgb: 前缀
  • 你连到了错误的环境 —— 资产 ID 在 signet 和主网之间不通用
解决方法:
  1. 检查接口端点路径和资源标识符,包括 /api/v1 前缀
  2. active_only=false 重新查询,看该交易对是否存在但被停用
  3. GET /api/v1/market/assets 动态发现 ID,不要硬编码,并按环境区分配置
  4. 检查响应里的 total —— 它是全部匹配项的数量,因此 total 大于你的分页大小说明你看到的是分页,而不是「不存在」
现象: 返回 RATE_LIMIT_EXCEEDED,通常发生在轮询价格时原因: 短时间内发送了过多请求。解决方法:
  1. 读取 X-RateLimit-RemainingX-RateLimit-Reset,在被切断之前就主动退避,而不是等 429 才反应
  2. 如果响应给出了等待时间,请在该时间之后重试
  3. 不要循环轮询 POST /market/quote —— 打开 WebSocket,在确实需要新价格时再发 quote_request
  4. 缓存 market/assetsmarket/pairs;两者都可安全缓存,且很少变化
  5. 记住限制是同时按 IP、按接口端点和按全局计算的,所以同一 NAT 后面一个吵闹的邻居可能会吃掉你的额度
现象: 返回 INTERNAL_ERROR,或者一分钟前还正常的接口端点返回 503原因:
  • 500 —— 服务端出现意外错误
  • 502 —— 从上游服务器收到了无效响应
  • 503 —— 做市方或 LSP 节点暂时无法访问、正在维护或已过载
这几种都不表示你的请求格式有误。解决方法:
  1. 使用带抖动的指数退避重试;这类失败属于瞬时故障
  2. 超时后不要盲目重试 /swaps/execute —— 先轮询 POST /api/v1/swaps/atomic/status,确认这笔调用是否其实已经生效
  3. 如果持续出现,请附上错误体中的 request_id 反馈给我们

报价与金额

现象: /swaps/init 拒绝了一个片刻之前还有效的 rfq_id原因: 报价的 expires_at 已过。这个窗口以数十秒计,不是数分钟。解决方法:
  1. 尽可能晚地取报价 —— 紧贴发起之前,绝不要在用户可能久留的确认界面之前
  2. 让发起 → 白名单 → 执行整条链路保持紧凑;它们全都基于同一个报价
  3. 如果用户犹豫,丢弃这个 rfq_id,等他继续时再请求新报价
现象: 返回 VALIDATION_ERROR,或一个提到金额的 400原因:
  • 低于该min_amount 或高于 max_amount —— 这些限制按层存放在每个资产的 endpoints 数组里,而不在资产本身上
  • 发送了展示单位而不是原始单位
  • 两侧都填了金额,或两侧都没填
解决方法:
  1. 从你所路由的那一层对应的 endpoints 条目中读取限制
  2. BTC 一侧用毫聪,RGB 一侧用该资产 precision 对应的原始单位 —— 见常见问题
  3. from_asset.amount(正向报价)和 to_asset.amount(反向报价)中恰好填一个
现象: 报价在算术上「不对」,差了 1,000 倍或 1 亿倍原因:
  • BTC 一侧被当作聪处理,而 API 指的是毫聪
  • 把一个资产的精度用在了另一个资产上 —— 精度是按资产定义的,BTC 和 USDT 并不共用一个
  • price 当成了展示汇率;它是一个完整单位的 from_assetto_asset 最小单位表示的价格
解决方法:
  1. 展示响应中的 to_asset.amount —— 手续费已经计入其中 —— 而不要用 price 反算
  2. GET /api/v1/market/assets 逐个资产读取 precision,绝不硬编码换算系数
  3. 在把换算逻辑接入界面之前,先在 signet 上用一笔小额报价做一次往返验证

交换

现象: init 返回了 swapstring,但 execute 失败原因: 这个 swapstring 从未在你自己的节点上加入白名单,因此接单方一侧无法履行 HTLC。这是最常见的集成故障,因为中间那一步并不是 Maker API 调用。解决方法: 三个步骤必须按顺序执行,且分布在两台不同的服务器上:
  1. Maker API 上调用 POST /api/v1/swaps/init → 返回 swapstringpayment_hashaccess_token
  2. 通过你自己的 RGB Lightning Node/taker API 把 swapstring 加入白名单
  3. Maker API 上调用 POST /api/v1/swaps/execute,带上 swapstringtaker_pubkeypayment_hash
同时确认 taker_pubkey 是你节点的 pubkey,且 payment_hash 与发起时返回的一致。参见交换协议
现象: 对一笔你确知存在的交换,POST /api/v1/swaps/atomic/status 返回 404原因: access_token 缺失或无效。这个 404 是刻意统一的 —— 它不区分「token 错误」和「不存在这笔交换」,这样该接口就无法被用来探测 payment hash。解决方法:
  1. 同时发送 payment_hash /swaps/init 返回的 access_token
  2. 如果 token 当时没有保存下来,它无法找回 —— 它只返回一次。请在发起时就把它和 payment hash 一起存好
  3. 确认你轮询的环境与这笔交换创建时所在的环境一致
现象: execute 返回了 200,但状态一直是 Pending,资产也没到账原因:
  • HTLC 还在途中 —— execute 返回并不等于结算完成
  • 资产一侧没有容量足够的路由
  • 你节点上待处理的 RGB 转账尚未推进
解决方法:
  1. 继续轮询 /swaps/atomic/status;终态是 SucceededExpiredFailed
  2. 在你的节点上刷新转账,并检查该资产的出向容量 —— 不要只看总余额
  3. 用交换对象上的 expires_at 与当前时间对比,判断 HTLC 还剩多少时间
现象: 状态变为 ExpiredFailed原因:
  • Expired —— 双方完成之前 HTLC 超时已过,通常是白名单或执行来得太晚
  • Failed —— HTLC 无法路由或结算,通常是容量或流动性问题
解决方法:
  1. 你的资金没有风险:未完成的原子交换会自动把双方资金退回。不要手动重新打款
  2. 新的报价重试 —— 旧的 rfq_idswapstring 已经作废
  3. 如果同一交易对反复失败,检查该资产通道上的容量,并通过 LSPS1 订购更多流动性

WebSocket

WebSocket 的错误不是 HTTP 响应 —— 它们以 JSON 消息的形式抵达已打开的连接;遇到严重错误时,服务器可能直接关闭连接。
现象: 连接已打开、quote_request 已发出,但没有 quote_response 返回原因: 请求失败了。失败会以带 error 字段的消息返回,而不是 quote_response,因此只监听 quote_response 的客户端看到的就是「一片安静」。解决方法:
  1. 打印所有入站帧,而不只是你期待的那些,并处理 error 结构
  2. 检查常见原因:未知资产、该交易对不支持的路由、超出按层限制的金额
  3. 确认 from_amount / to_amount 中恰好设置了一个
现象: 第一条报价到了,之后就再没有变化原因: 这个协议是请求/响应式的 —— 没有订阅机制。服务器只回答你发出的那条消息,不会主动推送更新。解决方法: 每当你需要当前价格时就发一条新的 quote_request,由你自己的定时器或用户操作来驱动。
现象: 连接意外关闭,有时发生在流程中途重连策略 —— 当连接意外关闭时:
  1. 等待几秒(5–10 秒)
  2. 重新建立连接
  3. 重新发送所有待处理的 quote_request 以获取新报价 —— 报价不会跨越重连留存
另外检查:
  • 定期发送 ping 并期待 pong;经过代理的空闲连接就是一条即将被关闭的连接
  • 每个会话使用一个新的唯一 {client_id},并确认 URL 是 wss:// 而不是 ws://
  • 如果你处在会剥离 WebSocket 升级的企业代理之后,请退回到 POST /market/quote

通道订单(LSPS1)

现象: 一旦带上 client_asset_amount,调用就失败原因:client_asset_amount > 0 时,客户端是在购买资产,因此需要一个来自 POST /api/v1/market/quote 的新鲜 rfq_id 来定价。解决方法:
  1. 先取报价,再把这个 rfq_id 传给 estimate_fees / create_order
  2. 如果报价在两次调用之间过期了,重新取一次
  3. 如果你只想要 LSP 一侧的流动性,就完全不要传 client_asset_amount —— 那种情况下不需要报价
现象: create_order 在余额或资产数量上校验失败原因: 请求超出了 LSP 公布的限制。解决方法:
  1. 读取 GET /api/v1/lsps1/get_info 返回的 options,把参数控制在 min_channel_balance_sat / max_channel_balance_sat、初始余额上下界以及 max_channel_expiry_blocks 之内
  2. 检查同一响应中按资产给出的上限 —— min_initial_lsp_amount / max_initial_lsp_amount 是按资产定义的,且可能为 0,那表示该资产不支持这一侧
  3. 下单之前先连接 lsp_connection_url 对应的 peer,这样通道才有地方开
现象: 通道始终没有开通,而 order_statePENDING_RATE_DECISION原因: 付款结算之前市场汇率出现了明显变动,因此 LSP 暂停了订单,而不是按过时价格开通通道。它在等你决定。解决方法: 调用 POST /api/v1/lsps1/rate_decision,带上 order_id、该订单的 access_tokenaccept_new_rate —— true 表示按当前汇率继续,false 表示触发退款到 refund_onchain_address。退款会返回 refund_txid。对处于其他状态的订单调用该接口会返回 400
现象: 你无法读回自己创建的订单原因: 与交换不同,这两种情况是可区分的 —— access_token 无效返回 400order_id 不存在返回 404解决方法:create_order 返回的 access_tokenorder_id 一起保存;它只在创建时返回,而 get_orderrate_decision 都需要它。如果想要一层兜底提醒,可以在下单时设置 email
现象: 付款已结算,order_stateCHANNEL_OPENING解决方法:
  1. 继续轮询 get_order —— 它可以安全轮询,且在通道存在之前 channel 一直是 null
  2. GET /api/v1/lsps1/network_info 返回的当前高度与 required_channel_confirmations 对比
  3. 注意付款的过期时间在 payment.bolt11.expires_atpayment.onchain.expires_at 下,通道自身的过期时间在 channel.expires_at 下 —— 顶层没有 expires_at 可读

重试与健壮性

把客户端设计成「坏响应是一种已处理的情况」,而不是一次崩溃。
  1. 优雅地处理错误。 解析全部三种信封,基于 error_code 分支判断,并给出可操作的提示,而不是把原始故障直接抛给用户。
  2. 只重试值得重试的。 瞬时故障 —— 500502503429 —— 值得再试一次,请使用带抖动的指数退避。客户端错误(400401403404422)无论发多少次,结果都一样。
  3. 尊重速率限制。 读取 X-RateLimit-* 请求头,主动待在上限之内,而不是靠撞上限来发现它。缓存资产、交易对之类的静态数据。
  4. 把交换执行当作非幂等操作。 /swaps/execute 超时并不能证明它失败了 —— 在重新发送任何东西之前,先轮询 /swaps/atomic/status
  5. 限制总重试次数。 面对 503 的无限重试循环,会变成你对自己发起的拒绝服务。

反馈之前

一份可复现的报告通常一个来回就能定位。
  1. 查看日志中完整的错误响应,而不只是状态码 —— 应用错误会带上 request_id,它把你的请求和我们的日志关联起来。
  2. 校验输入:确认每个参数和负载字段都符合该接口端点的要求,且金额使用的是原始单位。
  3. curl 复现,并隐去 API 密钥。
  4. 确认基础 URL 包含 /api/v1,并且指向你以为的那个环境。
  5. 试一下接口端点页面的交互式演练场(运行在 signet)—— 如果同样的调用在那里成功,差异就在你的客户端里。

获取帮助

如果是疑问而不是报错,请查看常见问题;规范与上游链接见更多资源 其他情况请从下面的渠道中选择一个反馈问题,并附上:
  1. 请求 URL 与 HTTP 方法
  2. 请求负载(请隐去 API 密钥)
  3. 完整的错误响应,包含 request_id
  4. 请求的时间戳
  5. 运行环境(signet / 主网)

Telegram 社群

向社群提问。

邮件支持

紧急问题的直接支持渠道。