Skip to main content
The KaleidoSwap API uses standard HTTP status codes and structured error bodies to report the outcome of a request. This page covers how to read an error, what each status code means, and the fix for the failures integrations actually hit. For questions rather than failures, see the FAQ.

Error Response Format

The API returns three distinct error envelopes depending on where the request failed. A client that handles all three is a client that never has to guess.

Application errors

Business and application errors (invalid parameters, missing resources, conflicts, rate limits, server errors) return a structured envelope:
  • error_code (string): Stable, machine-readable error code (e.g. PAIR_NOT_FOUND, VALIDATION_ERROR, NOT_FOUND, RATE_LIMIT_EXCEEDED, INTERNAL_ERROR).
  • message (string): Human-readable description of the error.
  • details (object): Optional structured context about the error (may be empty).
  • request_id (string): Request correlation identifier — include it when contacting support.
Branch on error_code, never on message. The codes are stable; the wording is not.

Request-validation errors (HTTP 422)

Requests that fail schema validation before reaching application logic return FastAPI’s validation envelope with HTTP 422:
  • detail (array<object>): One entry per validation failure.
    • loc (array<(string | integer)>): Path to the offending field.
    • msg (string): Validation error message.
    • type (string): FastAPI/Pydantic validation error type.

Legacy errors

A few endpoints still return some 400 responses as a bare detail object, without error_code or request_id — for example an unsupported route on POST /market/quote or a malformed pair_ticker filter on GET /market/pairs:
  • detail (string): Human-readable description of the error.
Clients handling 400 responses should accept both the structured envelope and this legacy shape.

HTTP Status Codes

The 4xx class means the request needs changing; the 5xx class means the request was fine and something upstream was not. Only the 5xx class plus 429 are worth retrying — see Retry and Resilience.

Requests and Authentication

Symptoms: 400, with either an error_code envelope or a bare detail stringCause: missing or invalid parameters in the request.Solutions:
  1. Verify the payload and confirm every required field is present with valid data
  2. Make your handler accept both 400 shapes — read error_code if present, fall back to detail
  3. For {"detail": "Route not supported for this pair..."}, check the pair’s actual routes with POST /api/v1/market/pairs/routes and send a from_asset.layerto_asset.layer combination that appears there
  4. For pair filters, pair_ticker must be BASE/QUOTE (e.g. BTC/USDT), and single-pair identifiers are mutually exclusive
Symptoms: requests that used to work start returning 401, or 401 on first call with a key attachedCauses:
  • API-key enforcement has been enabled for your environment and no valid key is being sent
  • The header is malformed — it must be Authorization: Bearer <token>, not the bare token
  • The key lacks the scope for that endpoint (quote:read for market data, swap:execute for swap init/execute)
Solutions:
  1. Log the exact Authorization header your client sends and check for a missing Bearer prefix or a trailing newline
  2. If the call is a swap init or execute, confirm the key carries swap:execute and not only quote:read
  3. Verify the key is valid and has not been revoked
  4. If you have no key yet, note that anonymous access still works on Signet — a 401 means enforcement is live for you
Symptoms: 403 on an endpoint that accepts your key elsewhereCause: the key is valid but not allowed from this origin.Solution: call from an allowed origin, or request that the origin be added. This is the failure mode you hit when a key intended for a backend is used from a browser or a new deployment host — see also the CORS note in the FAQ.
Symptoms: 422 with a detail array rather than an error_codeCause: the request failed schema validation before reaching application logic, so no business error was produced.Solution: read detail[].loc — it is the exact path to the offending field. ["body", "rfq_id"] with "type": "missing" means you omitted rfq_id entirely; a "type" of int_parsing usually means an amount was sent as a string. Do not treat 422 as retryable: the same payload will always fail.
Symptoms: PAIR_NOT_FOUND, NOT_FOUND, or an empty assets array for a ticker you know is supportedCauses:
  • The endpoint URL or a resource identifier is wrong
  • The asset or pair is inactive, and GET /market/pairs filters to active pairs by default (active_only=true)
  • You filtered by asset_id where the maker expects the RGB contract ID exactly, including the rgb: prefix
  • You are on the wrong environment — asset IDs are not portable between Signet and mainnet
Solutions:
  1. Check the endpoint path and resource identifiers, including the /api/v1 prefix
  2. Re-query with active_only=false to see whether the pair exists but is disabled
  3. Discover IDs from GET /api/v1/market/assets rather than hardcoding them, and key your config by environment
  4. Check total in the response — it is the count of all matches, so a total above your page size means you are looking at pagination, not absence
Symptoms: RATE_LIMIT_EXCEEDED, typically while polling for pricesCause: too many requests in a short period.Solutions:
  1. Read X-RateLimit-Remaining and X-RateLimit-Reset and back off before you are cut off, rather than reacting to the 429
  2. Retry after the indicated wait time, if one is provided
  3. Stop polling POST /market/quote in a loop — open the WebSocket and send a quote_request when you actually need a new price
  4. Cache market/assets and market/pairs; both are safe to cache and change rarely
  5. Remember the limits are per IP and per endpoint and global, so a noisy neighbour behind the same NAT can consume your budget
Symptoms: INTERNAL_ERROR, or 503 on endpoints that were working a minute agoCauses:
  • 500 — an unexpected error on the server
  • 502 — an invalid response from an upstream server
  • 503 — the maker or the LSP node is temporarily unreachable, under maintenance, or overloaded
None of these mean your request was malformed.Solutions:
  1. Retry with exponential backoff and jitter; these are the transient class of failures
  2. Do not blind-retry a /swaps/execute call on a timeout — poll POST /api/v1/swaps/atomic/status first to see whether it actually landed
  3. If it persists, report it with the request_id from the error body

Quotes and Amounts

Symptoms: /swaps/init rejects an rfq_id that was valid moments agoCause: the quote’s expires_at has passed. The window is tens of seconds, not minutes.Solutions:
  1. Quote as late as possible — immediately before init, never before a confirmation screen the user might sit on
  2. Keep the whole init → whitelist → execute sequence tight; it all runs against one quote
  3. If a user hesitates, discard the rfq_id and request a fresh quote when they resume
Symptoms: VALIDATION_ERROR or a 400 mentioning the amountCauses:
  • Below min_amount or above max_amount for that layer — the limits live per-layer in each asset’s endpoints array, not on the asset itself
  • Display units sent instead of raw units
  • An amount on both legs, or on neither
Solutions:
  1. Read the limits from the matching endpoints entry for the layer you are routing over
  2. Send BTC legs in millisatoshis and RGB legs in raw units per the asset’s precision — see the FAQ
  3. Set exactly one of from_asset.amount (forward quote) or to_asset.amount (reverse quote)
Symptoms: the quote is arithmetically “wrong” by a factor of 1,000 or 100,000,000Causes:
  • A BTC leg treated as satoshis when the API means millisatoshis
  • One asset’s precision applied to another — precision is per asset, and BTC and USDT do not share one
  • price read as a display rate; it is one whole from_asset unit expressed in the smallest unit of to_asset
Solutions:
  1. Display to_asset.amount from the response — the fee is already folded into it — instead of recomputing from price
  2. Pull precision per asset from GET /api/v1/market/assets and never hardcode a divisor
  3. Sanity-check a small quote round-trip on Signet before wiring the conversion into a UI

Swaps

Symptoms: init returned a swapstring, but execute failsCause: the swapstring was never whitelisted on your own node, so the taker side cannot honour the HTLC. This is the single most common integration failure, because the middle step is not a Maker API call.Solution: the three steps must run in order, against two different servers:
  1. POST /api/v1/swaps/init on the Maker API → returns swapstring, payment_hash, access_token
  2. Whitelist the swapstring via the /taker API of your own RGB Lightning Node
  3. POST /api/v1/swaps/execute on the Maker API with swapstring, taker_pubkey, and payment_hash
Also confirm taker_pubkey is your node’s pubkey and that the payment_hash matches the one from init. See Swap Protocol.
Symptoms: POST /api/v1/swaps/atomic/status returns 404 for a swap you know existsCause: a missing or invalid access_token. The 404 is deliberately uniform — it does not distinguish “wrong token” from “no such swap”, so the endpoint cannot be used to probe payment hashes.Solutions:
  1. Send both payment_hash and the access_token returned by /swaps/init
  2. If the token was never persisted, it cannot be recovered — it is returned only once. Store it with the payment hash at init time
  3. Check you are polling the environment the swap was created on
Symptoms: execute returned 200, but the status stays Pending and the assets have not arrivedCauses:
  • The HTLC is still in flight — execute returning is not settlement
  • No route with enough capacity on the asset leg
  • Pending RGB transfers on your node have not been advanced
Solutions:
  1. Keep polling /swaps/atomic/status; the terminal states are Succeeded, Expired, and Failed
  2. On your node, refresh transfers and check outbound capacity on the specific asset — not just the total balance
  3. Compare expires_at on the swap object against the current time to know how long the HTLC still has
Symptoms: status moves to Expired or FailedCauses:
  • Expired — the HTLC timeout elapsed before both sides completed, usually because whitelisting or execute came too late
  • Failed — the HTLC could not be routed or settled, typically a capacity or liquidity problem
Solutions:
  1. Your funds are not at risk: an incomplete atomic swap returns both sides’ funds automatically. Do not re-send funds manually
  2. Retry from a fresh quote — the old rfq_id and swapstring are spent
  3. If it fails repeatedly on the same pair, check capacity on that asset’s channel and order more liquidity via LSPS1

WebSocket

WebSocket errors are not HTTP responses — they arrive as JSON messages on the open socket, and on a critical error the server may close the connection outright.
Symptoms: the socket is open and the quote_request was sent, but no quote_response comes backCause: the request failed. Failures come back as a message with an error field instead of a quote_response, so a client that only listens for quote_response sees silence.Solutions:
  1. Log every inbound frame, not just the ones you expect, and handle the error shape
  2. Check the usual culprits: unknown asset, a route the pair does not support, an amount outside the per-layer limits
  3. Confirm you set exactly one of from_amount / to_amount
Symptoms: the first quote arrives, then nothing changesCause: the protocol is request/response — there is no subscription. The server answers the message you sent, and does not push updates.Solution: send a new quote_request whenever you need a current price, and drive it from your own interval or from user action.
Symptoms: the socket closes unexpectedly, sometimes mid-flowReconnection strategy — when a connection closes unexpectedly:
  1. Wait a few seconds (5–10 s)
  2. Re-establish the connection
  3. Re-issue any pending quote_request messages to obtain fresh quotes — quotes do not survive the reconnect
Also check:
  • Send periodic ping messages and expect a pong; an idle connection through a proxy is a connection about to be closed
  • Use a fresh unique {client_id} per session, and verify the URL is wss://, not ws://
  • If you are behind a corporate proxy that strips WebSocket upgrades, fall back to POST /market/quote

Channel Orders (LSPS1)

Symptoms: the call fails when you include client_asset_amountCause: when client_asset_amount > 0 the client is purchasing assets, so a fresh rfq_id from POST /api/v1/market/quote is required to price them.Solutions:
  1. Quote first, then pass that rfq_id into estimate_fees / create_order
  2. Requote if the quote expired between the two calls
  3. Omit client_asset_amount entirely if you only want LSP-side liquidity — no quote is needed then
Symptoms: create_order fails validation on balances or asset amountsCause: the request falls outside the LSP’s advertised limits.Solutions:
  1. Read options from GET /api/v1/lsps1/get_info and stay inside min_channel_balance_sat / max_channel_balance_sat, the initial-balance bounds, and max_channel_expiry_blocks
  2. Check the per-asset caps in the same response — min_initial_lsp_amount / max_initial_lsp_amount are per asset and can be 0, which means that side is not available for it
  3. Connect to the peer at lsp_connection_url before ordering, so the channel has somewhere to open
Symptoms: the channel never opens and order_state is PENDING_RATE_DECISIONCause: the market rate moved significantly before the payment settled, so the LSP paused the order instead of opening a channel at a stale price. It is waiting on you.Solution: call POST /api/v1/lsps1/rate_decision with the order_id, the order’s access_token, and accept_new_ratetrue to proceed at the current rate, false to trigger a refund to refund_onchain_address. Refunds return a refund_txid. Calling this endpoint on an order in any other state returns 400.
Symptoms: you cannot read back an order you createdCause: unlike swaps, these two are distinguishable — an invalid access_token returns 400, an unknown order_id returns 404.Solution: store the access_token from create_order alongside the order_id; it is returned only at creation and is required by both get_order and rate_decision. Set email on the order if you want notifications as a backstop.
Symptoms: payment settled, order_state is CHANNEL_OPENINGSolutions:
  1. Keep polling get_order — it is safe to poll, and channel stays null until the channel exists
  2. Compare required_channel_confirmations against the current height from GET /api/v1/lsps1/network_info
  3. Note that payment expiries live under payment.bolt11.expires_at and payment.onchain.expires_at, and the channel’s own expiry under channel.expires_at — there is no top-level expires_at to read

Retry and Resilience

Design the client so a bad response is a handled case rather than a crash.
  1. Handle errors gracefully. Parse all three envelopes, branch on error_code, and surface something actionable instead of propagating a raw failure to the user.
  2. Retry only what is retryable. Transient failures — 500, 502, 503, and 429 — are worth another attempt with exponential backoff and jitter. Client errors (400, 401, 403, 404, 422) will fail identically no matter how many times you send them.
  3. Respect the rate limits. Read the X-RateLimit-* headers and stay under the ceiling rather than discovering it. Cache static data such as assets and pairs.
  4. Treat swap execution as non-idempotent. A timeout on /swaps/execute is not proof of failure — poll /swaps/atomic/status before re-sending anything.
  5. Cap total attempts. An unbounded retry loop against a 503 becomes your own denial of service.

Before You Report

A reproducible report is usually diagnosed in one round trip.
  1. Check your logs for the full error response, not just the status code — application errors carry a request_id that correlates your request with our logs.
  2. Validate the inputs: confirm every parameter and payload field meets the endpoint’s requirements, and that amounts are in raw units.
  3. Reproduce with curl, with any API key redacted.
  4. Confirm the base URL includes /api/v1 and points at the environment you think it does.
  5. Try the interactive playground on the endpoint pages, which runs against Signet — if the same call succeeds there, the difference is in your client.

Get Help

Check the FAQ for questions rather than errors, and Additional Resources for specifications and upstream links. For anything else, report a problem through your preferred channel from the options below, including:
  1. Request URL and HTTP method
  2. Request payload (redact any API key)
  3. Full error response, including request_id
  4. Timestamp of the request
  5. Environment (Signet / Mainnet)

Telegram Community

Ask the community.

Email Support

Direct support for urgent issues.