Api

Error Codes

Complete error reference

This document provides a comprehensive reference for all error codes in the Hydra API.

gRPC Status Codes

Hydra uses standard gRPC status codes. All errors follow this format:

{
  "code": 3,
  "message": "Invalid request: unknown sending asset",
  "details": ["<one hydra_app.ErrorDetail, below>"]
}

The error detail

(2026-10-14) An error Hydra App answers a call with carries, beside its code and its message, an ErrorDetail whenever the node knows more than the two say: every refusal of its own — including a field missing, one that does not parse, a record it holds none of, and a call this build does not offer — every failure a network's node gives a reason for — a balance or a channel short of what the call needs, a peer not connected, a chain out of reach, a refused fee bump — and every failure it can say more of. A failure it relays from a service without a reason, and an error the transport answers before a method runs (a body that does not decode, a method that does not exist), carry none.

FieldTypeDescription
reasonFailureReasonWhy the call failed: a closed set to match on instead of the message — see Reasons. Absent (FAILURE_REASON_UNSPECIFIED) when the node does not know
retryRetryAdviceWhat clears the failure: RETRY_ADVICE_RESEND — send the same request again, nothing about it is wrong; RETRY_ADVICE_REBUILD — ask for the same operation again, which builds it afresh from what has moved on. Absent when nothing was said: a retry meets the same failure unless something changes first, which the reason names
outcome_unknownboolThe operation may still take effect: the failure is no proof that it did not happen. Undo, refund or repeat it only once its effect can be observed either way
metadatamap<string, string>The names and figures the reason comes with, decimals as strings; each reason below documents the ones it carries
error_idstringThe id the node logged the whole failure under, for a failure it logged as a warning or an error: quote it when you report the failure. Absent for a refusal of the request, which says all there is to it

gRPC carries it in the status details (the grpc-status-details-bin trailer): a google.rpc.Status with the status's own code and message, holding one Any of type type.googleapis.com/hydra_app.ErrorDetail, defined in error.proto of the /proto download. JSON-RPC carries it as the error's data, in the JSON mapping — enum values by name, fields in camelCase, absent fields left out:

{
  "code": -32602,
  "message": "Invalid request: unknown sending asset",
  "data": {
    "reason": "FAILURE_REASON_UNKNOWN_ASSET",
    "metadata": { "side": "sending" }
  }
}

The code and the message say what they always did, so a client that reads none of this is exactly as well off as before. Match on reason rather than on the message: messages are written for people, and their wording changes. A message is cut at 2 KiB, so the details fit beside it within the trailers' limit; a failure logged under an error_id is logged whole.

Python (grpcio with grpcio-status):

from grpc_status import rpc_status
import error_pb2  # generated from error.proto

def error_detail(error):  # error: grpc.RpcError
    status = rpc_status.from_call(error)
    for packed in status.details if status else []:
        if packed.Is(error_pb2.ErrorDetail.DESCRIPTOR):
            detail = error_pb2.ErrorDetail()
            packed.Unpack(detail)
            return detail
    return None

Rust (hydra-app-types): hydra_app_types::error_detail::error_detail(status.details()).

Reasons

Each reason is answered with the code below. A refusal of the venue's keeps the code the venue chose, which the venue does not fix for its prefixes; and a failure that may still take effect, or that a retry clears, is answered with DEADLINE_EXCEEDED, ABORTED or UNAVAILABLE whatever its reason. A client built before a reason was added reads a number it does not know: read the code and the message then.

ReasonCodeMeaning
FAILURE_REASON_MALFORMED_REQUESTINVALID_ARGUMENTA field the request needs is missing, does not parse, or holds a value, or a combination of values, the call does not take.
FAILURE_REASON_UNSUPPORTED_NETWORKINVALID_ARGUMENTThe request names a network this request does not take.
FAILURE_REASON_UNKNOWN_ASSETINVALID_ARGUMENTThe request names an asset this node does not know. Metadata: side (sending or receiving) on a swap.
FAILURE_REASON_INVALID_AMOUNTINVALID_ARGUMENTAn amount the call cannot take: zero or below where it needs a positive one, more than the protocol can represent, or finer than the asset divides into; or a request whose amounts are all zero.
FAILURE_REASON_TOLERANCE_OUT_OF_RANGEINVALID_ARGUMENTA price-change tolerance outside [0, 1).
FAILURE_REASON_SAME_ASSET_BOTH_SIDESINVALID_ARGUMENTA swap sending and receiving the same currency.
FAILURE_REASON_FEES_EXCEED_AMOUNTINVALID_ARGUMENTA swap whose fees take all of its amount.
FAILURE_REASON_LEASE_DURATION_REQUIREDINVALID_ARGUMENTA liquidity request that asks the provider for liquidity without a lease_duration.
FAILURE_REASON_FEE_RATE_TOO_LOWINVALID_ARGUMENTA fee rate below what the network, or the transaction a fee bump replaces, requires.
FAILURE_REASON_ALREADY_AT_RATEINVALID_ARGUMENTA fee bump at the rate the transaction already pays.
FAILURE_REASON_INVALID_ADDRESSINVALID_ARGUMENTAn address that is not one of the network's, or a peer address that does not parse.
FAILURE_REASON_INVALID_INVOICEINVALID_ARGUMENTAn invoice that does not decode, is for another network, or does not carry a valid signature of its issuer.
FAILURE_REASON_INVALID_PREIMAGEINVALID_ARGUMENTA preimage that does not hash to the payment hash it is offered for.
FAILURE_REASON_EXPIRY_TOO_SHORTINVALID_ARGUMENTAn expiry too short for the payment: no route, and no peer, takes a payment that expires that soon.
FAILURE_REASON_NOT_CONFIGUREDFAILED_PRECONDITIONThis node runs without what the request needs: a component, or a setting of the network's node. Metadata: component (signer, node, pricing, authentication, liquidity, dex or referral) for a component; absent for a setting.
FAILURE_REASON_PROTOCOL_NOT_SUPPORTEDFAILED_PRECONDITIONThis build of Hydra App does not include the protocol the request needs. Metadata: protocol, the protocol's name.
FAILURE_REASON_NETWORK_NOT_SERVEDFAILED_PRECONDITIONThis node runs no node for the network the request needs.
FAILURE_REASON_VENUE_NODE_UNKNOWNFAILED_PRECONDITIONThe venue's node on the network is not known to this node yet.
FAILURE_REASON_PROVIDER_NODE_UNKNOWNFAILED_PRECONDITIONThe liquidity provider advertises no node on the network.
FAILURE_REASON_INSUFFICIENT_BALANCEFAILED_PRECONDITIONThe balance the request spends is short of what it needs.
FAILURE_REASON_INSUFFICIENT_NATIVE_BALANCEFAILED_PRECONDITIONThe native balance cannot pay the fee, or the resources, of the transaction the request needs.
FAILURE_REASON_INSUFFICIENT_OFFCHAIN_BALANCEFAILED_PRECONDITIONThe off-chain balance cannot pay the fee this way; pay it on chain.
FAILURE_REASON_INSUFFICIENT_FUNDSFAILED_PRECONDITIONA fee bump the wallet cannot cover.
FAILURE_REASON_WOULD_CREATE_DUSTFAILED_PRECONDITIONThe operation would leave an output too small to spend, or one that cannot pay its own fee.
FAILURE_REASON_NO_LIQUIDITYFAILED_PRECONDITIONNo liquidity can carry the swap.
FAILURE_REASON_LEASE_TOO_LARGEFAILED_PRECONDITIONThe swap needs more leased liquidity than is available.
FAILURE_REASON_CHANNEL_HOLDS_SWAP_PAYMENTFAILED_PRECONDITIONThe channel holds the inbound payment of a swap still in flight, which a dispute would hand back to the peer: wait for the swap to settle or fail. Metadata: channel_id, and swap_ids, the swaps' ids separated by commas.
FAILURE_REASON_OPERATION_NOT_APPLICABLEFAILED_PRECONDITIONThe operation does not apply to the network's protocol; the message names the one that does.
FAILURE_REASON_INSUFFICIENT_CHANNEL_BALANCEFAILED_PRECONDITIONWhat this node's side of the channel, or of every channel the payment could take, can send is short of what the request needs.
FAILURE_REASON_INSUFFICIENT_ALLOWANCEFAILED_PRECONDITIONThe allowance the wallet granted the spender is short of what the operation pulls.
FAILURE_REASON_BELOW_CHANNEL_RESERVEFAILED_PRECONDITIONThe operation would take a side of the channel below the reserve it keeps while the channel is open; only a close pays the reserve out.
FAILURE_REASON_AMOUNT_BELOW_MINIMUMFAILED_PRECONDITIONAn amount below the smallest the protocol, the channel or the peer takes.
FAILURE_REASON_AMOUNT_ABOVE_MAXIMUMFAILED_PRECONDITIONAn amount above the largest the protocol, the channel or the peer takes.
FAILURE_REASON_CHANNEL_NOT_READYFAILED_PRECONDITIONThe channel is not ready for the operation yet: it is opening, confirming, or recovering its state.
FAILURE_REASON_CHANNEL_CLOSEDFAILED_PRECONDITIONThe channel is closing or closed: the operation no longer applies to it.
FAILURE_REASON_CHANNEL_UPDATE_IN_PROGRESSFAILED_PRECONDITIONAnother update of the channel is under way, an on-chain update, a splice or a close, and the operation can follow once it settles.
FAILURE_REASON_CHANNEL_LIFETIME_NOT_OVERFAILED_PRECONDITIONThe channel was sold to stay open for a lifetime that has not run out yet.
FAILURE_REASON_DISPUTE_PERIOD_NOT_OVERFAILED_PRECONDITIONThe dispute period of a channel closed on chain has not run out yet: what it settles can be redeemed once it has.
FAILURE_REASON_PEER_REFUSEDFAILED_PRECONDITIONThe peer refused the request as it stands, for its reserve, its balance, its policy or a deadline: build another request, or turn to another peer.
FAILURE_REASON_PEER_BLOCKEDFAILED_PRECONDITIONThis node refuses the peer.
FAILURE_REASON_PEER_INCOMPATIBLEFAILED_PRECONDITIONThis node and the peer cannot agree on what the operation needs, its features, terms or limits: a change of configuration, or an upgrade, has to come first.
FAILURE_REASON_PEER_MISBEHAVEDFAILED_PRECONDITIONThe peer answered with what the protocol proves false, a signature that does not verify among them, and this node turned it away.
FAILURE_REASON_NO_ROUTEFAILED_PRECONDITIONNo route reaches the recipient with the amount, the fees and the expiry the request asks for.
FAILURE_REASON_INVOICE_EXPIREDFAILED_PRECONDITIONThe invoice has expired.
FAILURE_REASON_TERMS_EXPIREDFAILED_PRECONDITIONThe terms the operation relies on, offered by an earlier answer, have ended: ask for them again.
FAILURE_REASON_PAYMENT_ALREADY_SETTLEDFAILED_PRECONDITIONThe payment has completed or failed already: nothing is left to settle.
FAILURE_REASON_PAYMENTS_IN_FLIGHTFAILED_PRECONDITIONPayments in flight on the channel keep the operation from going ahead: wait for them to settle, or reject them, first.
FAILURE_REASON_NO_EFFECTFAILED_PRECONDITIONThe request had no effect: the state it would change already holds what it asks for, or moved on while it was under way. Read the state again before asking again.
FAILURE_REASON_NOTHING_TO_SETTLEFAILED_PRECONDITIONNothing is left for the operation to settle: what it would settle is settled already, being settled, or empty.
FAILURE_REASON_HTLC_TERMS_MISMATCHFAILED_PRECONDITIONThe HTLC does not carry the terms agreed for it, its script, amount, recipient, hash or expiry, so it can never be claimed as agreed.
FAILURE_REASON_HTLC_NOT_EXPIREDFAILED_PRECONDITIONThe HTLC's timelock has not expired, so it cannot be refunded yet.
FAILURE_REASON_HTLC_ALREADY_SETTLEDFAILED_PRECONDITIONThe HTLC was claimed or refunded already.
FAILURE_REASON_FEE_BUMP_REFUSEDFAILED_PRECONDITIONThe transaction's fee cannot be raised. Metadata: refusal, why, as GetBumpEligibility names it (for example BUMP_REFUSAL_NO_LONGER_PENDING).
FAILURE_REASON_ASSET_NOT_FOUNDNOT_FOUNDNo asset with that id.
FAILURE_REASON_SIMPLE_SWAP_NOT_FOUNDNOT_FOUNDNo simple swap in progress with that id.
FAILURE_REASON_CHANNEL_NOT_FOUNDNOT_FOUNDNo channel with that id.
FAILURE_REASON_PAYMENT_NOT_FOUNDNOT_FOUNDNo payment with that id.
FAILURE_REASON_TRANSACTION_NOT_FOUNDNOT_FOUNDNo transaction with that id.
FAILURE_REASON_LIQUIDITY_REQUEST_NOT_FOUNDNOT_FOUNDNo liquidity request with that id.
FAILURE_REASON_FIAT_PRICE_NOT_FOUNDNOT_FOUNDNo fiat price for the asset.
FAILURE_REASON_ASSET_CHANNEL_NOT_FOUNDNOT_FOUNDThe channel holds no asset channel for that asset.
FAILURE_REASON_HTLC_NOT_FOUNDNOT_FOUNDNo HTLC with that id.
FAILURE_REASON_CLIENT_REQUEST_ID_TAKENALREADY_EXISTSThe client_request_id already names a different liquidity request. Metadata: payment_uuid, the request it names.
FAILURE_REASON_PAYMENT_ALREADY_EXISTSALREADY_EXISTSA payment with the same hash or id was made before.
FAILURE_REASON_AUTHENTICATION_REJECTEDUNAUTHENTICATEDThe authentication service refused this node's identity. Metadata: rejection (for example NOT_WHITELISTED).
FAILURE_REASON_PRUNE_CANCELLINGUNAVAILABLEAn archive prune is winding down after a cancellation.
FAILURE_REASON_TOO_MANY_SUBSCRIBERSRESOURCE_EXHAUSTEDThe event stream has as many subscribers as it takes.
FAILURE_REASON_FEE_ESTIMATE_UNAVAILABLEUNAVAILABLENo fee estimate came within the time the call allows for one, and nothing was sent: send the request again.
FAILURE_REASON_SERVER_DEADLINEDEADLINE_EXCEEDEDThe call outlived the server's deadline, and may still take effect: it carries outcome_unknown.
FAILURE_REASON_PEER_NOT_CONNECTEDUNAVAILABLEThis node is not connected to the peer the operation needs, or its session with the peer is not established yet.
FAILURE_REASON_PEER_UNREACHABLEUNAVAILABLEThe peer could not be reached.
FAILURE_REASON_PEER_UNRESPONSIVEUNAVAILABLEThe peer did not answer in time.
FAILURE_REASON_CHAIN_UNAVAILABLEUNAVAILABLEThe chain's backend, an RPC node or an indexer, could not be reached or did not answer in time.
FAILURE_REASON_NODE_SYNCINGUNAVAILABLEThe network's node is still catching up with the chain.
FAILURE_REASON_CHANNEL_BUSYUNAVAILABLEThe channel is busy with a step that clears within a round trip with the peer.
FAILURE_REASON_CHANNEL_OUT_OF_SYNCUNAVAILABLEThis node's record of the channel disagrees with the chain's, or with the peer's, and the two have to be brought back in line first.
FAILURE_REASON_NODE_BUSYUNAVAILABLEThe network's node could not carry out the operation in the time it allows, held up by other work.
FAILURE_REASON_VENUE_BELOW_MINIMUMthe venue'sBelow the market's minimum. Metadata: market, side, min_place, min_fill.
FAILURE_REASON_VENUE_BELOW_BEST_FILLthe venue'sBelow the smallest fill the best price allows. Metadata: market, min_fill, min_fill_unit (base or quote), price.
FAILURE_REASON_VENUE_POST_ONLY_WOULD_CROSSthe venue'sA post-only order that would take. Metadata: market, price, best.
FAILURE_REASON_VENUE_NOTHING_TO_TAKEthe venue'sA taking order with nothing to take at its price. Metadata: market, price, best.
FAILURE_REASON_VENUE_FILL_OR_KILL_UNFILLEDthe venue'sA fill-or-kill order the book cannot fill whole. Metadata: market, got, got_unit, fillable, fillable_unit (each base or quote), price.
FAILURE_REASON_VENUE_SELF_TRADE_PREVENTEDthe venue'sAn order that would trade against the client's own. Metadata: market, resting_order_id.
FAILURE_REASON_VENUE_STOPPINGthe venue'sThe venue is stopping.
FAILURE_REASON_VENUE_ASSET_NOT_LISTEDthe venue'sThe currency is not listed. Metadata: currency.
FAILURE_REASON_VENUE_MARKET_STATEthe venue'sThe market does not take orders in its state. Metadata: market, state.
FAILURE_REASON_VENUE_MAINTENANCEthe venue'sThe venue is in maintenance.
FAILURE_REASON_VENUE_HUB_UNREACHABLEthe venue'sThe venue cannot reach the hub on the network. Metadata: network.
FAILURE_REASON_VENUE_SUB_ECONOMIC_CLIENT_LIMITthe venue'sThe client's channels would hold more than closing them is worth. Metadata: force_close_usd, added_usd, allowance_usd.
FAILURE_REASON_VENUE_ONCHAIN_LEG_FLOORthe venue'sAn on-chain leg below what the venue settles on chain. Metadata: currency, got, usd, floor.
FAILURE_REASON_VENUE_AT_RISK_CAPthe venue'sThe hub's capital at risk on the network would pass its cap. Metadata: network, projected, cap.
FAILURE_REASON_VENUE_SUB_ECONOMIC_HTLC_CAPthe venue'sA payment above what a channel carries economically. Metadata: network, max.
FAILURE_REASON_VENUE_SUB_ECONOMIC_VENUE_LIMITthe venue'sThe venue's standing in sub-economic payments is at its limit. Metadata: standing.
FAILURE_REASON_VENUE_MATCHER_OVERLOADEDthe venue'sThe matcher is shedding load. Metadata: state, class.
FAILURE_REASON_VENUE_CLIENT_ORDER_ID_TAKENthe venue'sThe client_order_id already names a different order. Metadata: order_id.
FAILURE_REASON_VENUE_MAKER_UNAVAILABLEthe venue'sNo maker of the match can settle now.
FAILURE_REASON_VENUE_INSUFFICIENT_CAPACITYthe venue'sA channel leg lacks the capacity the order needs. Metadata: currency, leg (sending or receiving), required, available.
FAILURE_REASON_REFERRAL_NOT_ELIGIBLEPERMISSION_DENIEDThis node is not eligible for invites.
FAILURE_REASON_REFERRAL_EPOCH_GATEDFAILED_PRECONDITIONInvites are not open in this epoch.
FAILURE_REASON_REFERRAL_CODE_EXPIREDFAILED_PRECONDITIONThe code has expired.
FAILURE_REASON_REFERRAL_SELF_REDEEMFAILED_PRECONDITIONA node cannot redeem its own code.
FAILURE_REASON_REFERRAL_QUOTA_EXHAUSTEDRESOURCE_EXHAUSTEDThe quota of codes is used up.
FAILURE_REASON_REFERRAL_CODE_NOT_FOUNDNOT_FOUNDNo such code.
FAILURE_REASON_REFERRAL_CODE_ALREADY_REDEEMEDALREADY_EXISTSThe code was redeemed already.
FAILURE_REASON_REFERRAL_ELIGIBILITY_EXISTSALREADY_EXISTSThis node is eligible already.
FAILURE_REASON_CALL_NOT_SUPPORTEDUNIMPLEMENTEDThis build of Hydra App does not offer the call on the network's protocol: support for the protocol is not compiled in, or the protocol has no such operation.

Common Status Codes

OK (0)

Description: Success - no error

When it occurs: Successful operation

Action: None required


CANCELLED (1)

Description: Operation was cancelled

When it occurs:

  • Client cancelled request
  • Stream was closed

Action: Retry if needed


UNKNOWN (2)

Description: Unknown error

When it occurs:

  • Unexpected server error
  • Unhandled exception

Action: Check request parameters, contact support if persists


INVALID_ARGUMENT (3)

Description: Client specified an invalid argument

Common causes:

  • Invalid asset ID format
  • Invalid network parameters
  • Malformed request data
  • Out-of-range values

Examples:

// Error: invalid asset id
{
  "code": 3,
  "message": "invalid asset id"
}

// Error: invalid network
{
  "code": 3,
  "message": "unsupported network protocol"
}

// Error: invalid amount
{
  "code": 3,
  "message": "amount must be positive"
}

Action:

  • Verify asset ID format (zero-padded hash for native assets, contract address for ERC-20s — see asset.GetNativeAsset / asset.GetAssets)
  • Check Network shape: { protocol, id } only — not chain_id/name
  • Validate all numeric values (DecimalString is human-readable, not base units)
  • Ensure required fields and one-of variants are present (e.g. exactly one fee_payment on Lease requests)

DEADLINE_EXCEEDED (4)

Description: The request ran out of time, and the operation may still have completed.

Don't read it as a failure. A read can simply be retried; for a write — a payment, an order, a channel operation — check whether it took effect before sending it again. A request that ran out of time without doing anything answers UNAVAILABLE instead, which is safe to retry.

The server's request deadline is 30 s per call. (2026-10-14) Over JSON-RPC it bounds each call on its own, wherever the call arrives: every call of a batch has its own 30 s, over HTTP or over a WebSocket. A call cut off there answers its own JSON-RPC error -32002 — over HTTP inside an ordinary 200 response, never an empty-bodied HTTP 504 — while the batch's other calls answer as usual. See the request deadline on JSON-RPC.

Three LiquidityService RPCs are exempt from that deadline — the deprecated RequestChannelLiquidity, RequestChannelRelease and RequestChannelLeaseExtension, over gRPC and (2026-10-14) over JSON-RPC alike. Each waits until the liquidity service has carried the request out, which on the on-chain fee rail means waiting through the fee transfer's confirmations: past 30 s, minutes on Ethereum. Set a client deadline of minutes on those three, or you will abandon a request whose fee has already been paid. (2026-10-14) A JSON-RPC subscription call is exempt too: it answers once its subscription is accepted.

(2026-10-14) Their replacements — SubmitChannelLiquidityRequest, SubmitChannelReleaseRequest and SubmitChannelLeaseExtensionRequest — are not exempt. They answer once your node has settled its part of the fee, normally within seconds, and the request is then followed to its end; see Submitting and following a request.

Action:

  • Retry with exponential backoff — only if the RPC is idempotent; see Idempotency & Retry Safety
  • Give chain-bound calls a client deadline measured in minutes, not seconds
  • For anything you want to watch rather than wait on, use the streaming endpoints

A timeout is not a rollback. The operation may have completed server-side after your deadline fired. For a write, verify before retrying.

Recovering a Submit* call that was cut off. (2026-10-14) The request may already have been made: the submission runs on in your node after its caller is gone. Send the same request again under the same client_request_id — it answers with the request already made, waiting for the first call if that is still running, and makes the request only if none was made. GetLiquidityRequest by the id reads it without submitting. Without an id, find the request on SubscribeLiquidityRequests or in GetFeePayments before you submit again. See client_request_id.


NOT_FOUND (5)

Description: Requested resource not found

Common causes:

  • Transaction doesn't exist
  • Order not found
  • Channel doesn't exist
  • Payment not found

Examples:

// Error: transaction not found
{
  "code": 5,
  "message": "transaction not found: abc123..."
}

// Error: order not found
{
  "code": 5,
  "message": "order not found: order_xyz789"
}

Action:

  • Verify resource ID is correct
  • Check that resource exists on the specified network
  • Ensure resource hasn't been deleted

ALREADY_EXISTS (6)

Description: Resource already exists

When it occurs:

  • Attempting to create duplicate resource
  • Market already initialized
  • Channel already opened
  • orderbook.CreateOrder of a different order under a client_order_id that already names one (prefix client_order_id_taken)
  • (2026-10-14) A liquidity.Submit* call of a different request under a client_request_id that already names one (client_request_id_taken: payment_uuid=…, naming that request — see Liquidity Service)

Examples:

{
  "code": 6,
  "message": "market already initialized"
}

Action:

  • Check if resource already exists
  • Use update/modify operations instead of create

PERMISSION_DENIED (7)

Description: Caller doesn't have permission

When it occurs:

  • Authentication failure
  • Insufficient permissions
  • Attempting to access another user's resource

Action:

  • Verify authentication credentials
  • Check API key permissions
  • Ensure you own the resource

RESOURCE_EXHAUSTED (8)

Description: A limit on how much of something you may use at once is reached

Common causes:

  • A stream already has as many subscribers as it takes (TOO_MANY_SUBSCRIBERS)
  • Your referral quota is used up (REFERRAL_QUOTA_EXHAUSTED)
  • The hub or the liquidity service refuses a request for a quota of its own — a rate limit — and Hydra App passes its code on

A balance, a channel or a liquidity pool too small for the request is not this code: it is FAILED_PRECONDITION (9).

Examples:

// Error: a stream with no room for another subscriber
{
  "code": 8,
  "message": "too many subscribers"
}

Action:

  • Close a subscription you no longer read before opening another
  • Back off and retry later: a quota frees up as earlier requests complete

FAILED_PRECONDITION (9)

Description: Operation rejected due to system state

Common causes:

  • Channel not active yet
  • Market not initialized
  • Asset not supported
  • Duration out of allowed range
  • Not enough balance on chain or in a channel (INSUFFICIENT_BALANCE, INSUFFICIENT_NATIVE_BALANCE, INSUFFICIENT_CHANNEL_BALANCE), and any refusal whose wording says something is insufficient
  • No liquidity for the request (NO_LIQUIDITY), or a lease larger than what is available (LEASE_TOO_LARGE)

Examples:

// Error: channel not ready
{
  "code": 9,
  "message": "channel not active for sending"
}

// Error: insufficient balance
{
  "code": 9,
  "message": "insufficient onchain balance"
}

// Error: market not initialized
{
  "code": 9,
  "message": "market not initialized for this pair"
}

// Error: duration invalid
{
  "code": 9,
  "message": "lease duration below minimum"
}

Action:

  • Wait for prerequisite conditions
  • Initialize required resources first
  • Check state before operations
  • Verify parameter ranges
  • Add funds, or try a smaller amount, where a balance or liquidity falls short

ABORTED (10)

Description: Operation was aborted

When it occurs:

  • Concurrent modification conflict
  • Transaction conflict
  • Order already cancelled

Action: Retry operation


OUT_OF_RANGE (11)

Description: Operation outside valid range

Common causes:

  • Amount too large or too small
  • Fee rate invalid
  • Duration out of range

Action:

  • Check minimum/maximum values
  • Adjust parameters to valid range

UNIMPLEMENTED (12)

Description: Operation not implemented

When it occurs:

  • Feature not yet available
  • Network not supported

A deprecated method is not this code: it keeps answering as before until it is removed.

Action:

  • Check API documentation for supported features
  • Use alternative endpoint

INTERNAL (13)

Description: Internal server error

When it occurs:

  • Database error
  • Backend service failure
  • Unexpected server condition

Action:

  • Retry with exponential backoff
  • Contact support if persists

UNAVAILABLE (14)

Description: Service temporarily unavailable

Common causes:

  • Service maintenance
  • Network issues
  • Backend overload
  • A wait inside Hydra App ran out before the operation was sent — for a peer, a chain provider, a lock. The message may say "timeout" or "deadline"; nothing took effect, and the request may be sent again

Action:

  • Retry with exponential backoff
  • Check status page
  • Wait and retry

DATA_LOSS (15)

Description: Unrecoverable data loss

When it occurs: Rare - serious server issue

Action: Contact support immediately


UNAUTHENTICATED (16)

Description: Request lacks valid authentication

When it occurs:

  • Missing authentication
  • Invalid credentials
  • Expired session

Action:

  • Provide authentication credentials
  • Refresh authentication token
  • Check API key is valid

Error Handling Best Practices

1. Implement Retry Logic — for idempotent RPCs only

⚠️ Use this helper only for Get*, Estimate*, and stream-reconnect calls. Wrapping SendTransaction, OpenChannel, CreateOrder, etc. in a blind retry can cause double-execution. See Idempotency & Retry Safety for the full rules.

async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation()
    } catch (error: any) {
      const code = error.code

      // Don't retry client errors (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED,
      // OUT_OF_RANGE, UNIMPLEMENTED, UNAUTHENTICATED, FAILED_PRECONDITION)
      if ([3, 5, 7, 9, 11, 12, 16].includes(code)) {
        throw error
      }

      // Retry on transient errors (DEADLINE_EXCEEDED, UNAVAILABLE) and INTERNAL
      if ([4, 13, 14].includes(code)) {
        const delay = baseDelay * Math.pow(2, i) + Math.floor(Math.random() * 500)
        console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`)
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }

      throw error
    }
  }

  throw new Error('Max retries exceeded')
}

// ✅ Safe — Get* is idempotent
const balance = await withRetry(() => walletClient.getBalance(request, {}))

// ❌ DANGEROUS — SendTransaction is NOT idempotent.
//    A DEADLINE_EXCEEDED retry can broadcast twice.
//    Use the verify-before-retry pattern from the Idempotency section.
// const tx = await withRetry(() => clientClient.sendTransaction(request, {}))

2. Graceful Error Handling

async function safeGetBalance(
  client: WalletServiceClient,
  network: Network,
  assetId: string
): Promise<Balance | null> {
  try {
    const request = new GetBalanceRequest()
    request.setNetwork(network)
    request.setAssetId(assetId)

    const response = await client.getBalance(request, {})
    return response.getBalance()
  } catch (error) {
    switch (error.code) {
      case 3:
        console.error('Invalid asset ID:', assetId)
        return null

      case 5:
        console.error('Asset not found:', assetId)
        return null

      case 14:
        console.warn('Service unavailable, retrying...')
        // Implement retry logic
        return null

      default:
        console.error('Unexpected error:', error)
        throw error
    }
  }
}

3. Check Preconditions

async function sendPaymentSafely(
  walletClient: WalletServiceClient,
  nodeClient: NodeServiceClient,
  network: Network,
  assetId: string,
  amount: string
) {
  // Check balance first
  const balanceReq = new GetBalanceRequest()
  balanceReq.setNetwork(network)
  balanceReq.setAssetId(assetId)

  const balanceResp = await walletClient.getBalance(balanceReq, {})
  // Size a single payment off max_sendable, not free_local — a per-payment
  // ceiling can bind below the balance.
  const available = balanceResp.getBalance()?.getOffchain()?.getMaxSendable()?.getValue()

  // Amounts are decimal strings in whole units — compare as decimals, not BigInt.
  if (new Big(available || '0').lt(new Big(amount))) {
    throw new Error('Insufficient balance')
  }

  // Proceed with payment
  const paymentReq = new SendPaymentRequest()
  // ... configure payment
  return await nodeClient.sendPayment(paymentReq, {})
}

4. User-Friendly Messages

function getUserFriendlyError(error: any): string {
  switch (error.code) {
    case 3:
      return 'Invalid input. Please check your entries.'

    case 5:
      return 'Resource not found. Please verify the ID.'

    case 8:
      return 'Too many requests at once. Please try again later.'

    case 9:
      if (error.message.includes('insufficient')) {
        return 'Insufficient funds. Please add more, or try a smaller amount.'
      }
      if (error.message.includes('liquidity')) {
        return 'Not enough liquidity available. Try a smaller amount.'
      }
      return 'Operation cannot be completed right now. Please try again later.'

    case 14:
      return 'Service temporarily unavailable. Please try again in a moment.'

    default:
      return 'An unexpected error occurred. Please try again.'
  }
}

// Usage in UI
try {
  await client.someOperation(request, {})
} catch (error) {
  alert(getUserFriendlyError(error))
}

Service-Specific Errors

Wallet Service

ErrorCauseSolution
INVALID_ARGUMENT: invalid asset idWrong asset ID formatCheck asset ID for network type
NOT_FOUND: transaction not foundTransaction doesn't existVerify txid is correct

Orderbook Service

ErrorCauseSolution
FAILED_PRECONDITION: no liquidityEmpty orderbookWait or provide liquidity
FAILED_PRECONDITION: market not initializedMarket doesn't existCall InitMarket first
INVALID_ARGUMENT: amount below minimumAmount too smallIncrease to min_base_amount
INVALID_ARGUMENT: ... out of rangeA price, an amount, or an amount taken at its price, past what the market can hold: the book counts both in whole units of the market's precision, up to 2⁶³−1Send a price and amount within the market's range
-32602 unknown field 'addLiquidity'Used the removed add_liquidity order variant (removed 2026-06-11)Provide liquidity with limit_orders instead — see Orderbook → Create Order
INVALID_ARGUMENT: on-chain settlement not allowed on maker orderSet settlement/route_filter on a resting maker (limit) orderOn-chain settlement is taker-only; omit it on maker orders

Swap Service

ErrorCauseSolution
FAILED_PRECONDITION: insufficient balanceNot enough offchain funds for a plain SwapUse SimpleSwap, which sets the channels up for you
FAILED_PRECONDITION: price exceeded tolerancePrice moved past price_change_tolerance between estimate and executeRe-estimate and retry; widen the tolerance on volatile pairs

Most simple-swap refusals are not errors at all. EstimateSimpleSwap succeeds and returns a SimpleSwapEstimate variant — no_liquidity, fees_higher_than_amount, lease_too_big, insufficient_sending_balance, insufficient_native_balance. Branch on the one-of; a switch that only handles the three success arms will misreport every one of these.

Node Service

ErrorCauseSolution
FAILED_PRECONDITION: channel not activeChannel still opening / confirmingWait for the Active event from SubscribeNodeEvents
NOT_FOUND: channel not foundInvalid channel_idVerify with watchOnlyNode.GetChannels
FAILED_PRECONDITION: insufficient capacityChannel too smallDeposit more funds via node.DepositChannel or liquidity.SubmitChannelLiquidityRequest (deposit op)
INVALID_ARGUMENT: peer not in zero-conf whitelistTried zero-conf with non-whitelisted peerAdd peer via node.AddPeerToZeroConfWhitelist first
INVALID_ARGUMENT from node.UpdateChannel, contributions cancel outThe chain refuses an update that leaves the channel holding what it already heldContributions that cancel are a payment, not an update — send it as one. See Update Channel
INVALID_ARGUMENT from node.UpdateChannel on peer_contributionUsed deposit: { all: {} } on the peer's side, where All would resolve against a wallet your node cannot seeName an exact amount for a peer deposit
FAILED_PRECONDITION: funding allowance exceededPeer-initiated deposit/payment exceeds set allowanceRaise via node.IncreaseFundingAllowance

Placement rejections (Orderbook)

Added 2026-08-13.

When the hub refuses an order, it encodes the reason in a stable, parseable message so an SDK can react without scraping prose:

<prefix>: key=value key=value — human tail

Match on the prefix and the key=value tokens only — never on the human tail, which is free text and may change. The gRPC status code is deliberately not part of the contract; the prefix is.

PrefixKeysMeaning
below_minimummarket, side, got, and exactly one of min_place / min_fillOrder size is under a placement minimum. The key present names the larger, binding one: min_place = the hub's USD placement floor at live prices; min_fill = the structural minimum.
below_best_fillside, got, min_fill, price, marketStrict price priority: the best order this one would have to take first rests at price, and its smallest fill, min_fill, is more than the order has (got; both in the unit side names). A taker takes nothing past that order, and a limit order would rest across it. Nothing was created: re-place at least min_fill, or at a price that does not reach price — see Price priority. Answered with FAILED_PRECONDITION
asset_not_listedcurrencyThat currency isn't listed on the hub
market_statemarket, stateThe market does not take this order in its current state: post_only (refuses market and swap orders, and limit orders that only take — IOC, FOK), cancel_only or frozen
post_only_would_crossprice, best, marketA post-only limit order at price — or any limit order on a MARKET_STATE_POST_ONLY market — would have taken an order on offer, the best at best. Nothing was created. Re-price so it does not cross. Answered with FAILED_PRECONDITION
nothing_to_takemarket, and price / best when there is oneAn order that only takes — a TIME_IN_FORCE_IOC limit order, or a market order — found nothing it may take within price (absent for a market order without limit_price). best is the best price an order it may take rests at on the other side, absent when there is none. Nothing was created. Answered with FAILED_PRECONDITION. A market order over an empty side used to be answered with an unprefixed INVALID_ARGUMENT
fill_or_kill_unfilledside, got, fillable, price, marketA TIME_IN_FORCE_FOK limit order for got could take only fillable at price or better (both in the unit side names). Nothing was created, and the book was left as it was. Answered with FAILED_PRECONDITION
self_trade_preventedresting_order_id, marketThe order's self_trade_prevention stopped it at resting_order_id, a resting order of yours, before it filled anything. Nothing was created. Answered with FAILED_PRECONDITION
venue_stopping—The hub is restarting and turns no new match into a swap until it is back: the order ended with nothing filled. Not a fault of the order: send it again once the hub is back. Answered with UNAVAILABLE
sub_economic_client_limitforce_close_usd, added_usd, allowance_usd(2026-09-16) Force-closing this client's in-flight sub-economic swaps already costs force_close_usd, and this match would add added_usd on top of an allowance of allowance_usd
sub_economic_htlc_capnetwork, node, current, addition, maxThe peer already holds its maximum concurrent sub-economic HTLC legs
sub_economic_venue_limitforce_close_usd, added_usd, admissible_usd, standing(2026-09-16) The same figure venue-wide has no room for this match. admissible_usd rises with standing (settled volume, [0, 1])
onchain_leg_floorcurrency, got, usd, floorAn on-chain leg of got in currency, worth usd, is below the floor the venue settles on chain
at_risk_capnetwork, projected, capThe hub's capital at risk on network would reach projected, past its cap: try a smaller order, or later
venue_maintenance—The venue is in maintenance and takes no order: send it again once maintenance ends
hub_unreachablenetworkThe venue cannot reach the hub on network right now
matcher_overloadedstate, classThe matcher is shedding load
client_order_id_takenorder_idThe client_order_id already names a different order of yours — live, or ended within the hub's retention period (24 hours by default). order_id is that order: resend it unchanged to get it back, or place this one under a new id. Hydra App answers it with ALREADY_EXISTS
maker_unavailable—None of the makers the order matched can settle with it right now, so no swap was made and nothing of the order filled: every maker was skipped as the match's swaps were created — the order may already have been announced on the event stream, and ended there — or, (2026-10-14) found before matching, the hub cannot pay one of them its leg, and nothing was created. Names no maker. Not a fault of the order: retry later; until that maker's channel frees, a retry can meet the same maker. Answered with FAILED_PRECONDITION
insufficient_capacitycurrency, leg, required, available(2026-10-14) The order needs required of your leg capacity (sending or receiving) in currency, more than the available the refusing check found left for it. Nothing was created: size the order to fit. At placement available is your GetOrderbookBalances sending / receiving, and a limit order's required on the receiving leg is what it receives in full at the market's max_maker_discount, so a maker rebate adds to it — see Sizing an order to fit your balance. An order that would trade is also refused with leg=receiving when the hub cannot forward its own fills now (available is then what the hub can forward to you), and a market order whose match took more than its estimate with required the excess. The human tail still reads Not enough <leg> capacity to place this order, …, the sentence this refusal carried before it had a prefix; match the prefix. Answered with FAILED_PRECONDITION

Anything not matching this closed set — including every message from a pre-2026-08-13 hub — is unclassified and should be treated as an opaque error, exactly as before.

(2026-10-14) Hydra App answers an unclassified refusal with the hub's own code — INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS, FAILED_PRECONDITION or RESOURCE_EXHAUSTED — unless its wording names a condition that has a code of its own: a node that is not connected to the hub is still UNAVAILABLE. Over JSON-RPC that is -32602, -32004, -32005, -32010 or -32003. Before, such a refusal reached you as INTERNAL (-32603).

The three sub-economic prefixes were renamed on 2026-09-16, and their keys with them. They were below_floor_budget and below_floor_count when this page was first written. A parser matching the old names now falls through to unclassified — which is safe (the order is still refused, and the refusal is still retryable) but loses the numbers that say what to do about it. The amounts are force-closure costs, not trade notionals: see Get Sub-Economic Policy for what they are measured in and how to predict a refusal instead of discovering it.

All three are retryable and none of them invalidates cached MarketInfo. sub_economic_client_limit and sub_economic_venue_limit clear as your in-flight small swaps settle; sub_economic_htlc_cap clears as their HTLCs resolve.

Only below_minimum invalidates your cached MarketInfo. It means the hub's live minimums moved past your cached view, so refetch GetMarketInfo and resize from the min_place= in the rejection — that value is authoritative, the min_place_* snapshot in MarketInfo is only advisory. below_best_fill does not: no minimum moved, the order met a resting order it is too small for.

market_state does not qualify: the market's state is what changed, and the market_info event on the market stream carries the new one the moment it changes — no refetch needed.

HTLC & Preimage Service

ErrorCauseSolution
-32601 method not found: node_registerPreimageCalled the removed node.RegisterPreimage (removed 2026-06-11)Call preimage.SettlePreimage — same request fields
FAILED_PRECONDITION from htlc.CreateHtlcSettlementTx / DeriveHtlcAddress / VerifyHtlcByLockTxidCalled a UTXO-model-only RPC on an account-model (EVM / Tron) networkThese pre-sign / fund-by-address flows apply to UTXO protocols only; account-model HTLCs settle permissionlessly
FAILED_PRECONDITION: on-chain HTLCs unavailableNetwork has no htlc_factory_address (EVM / Tron) configuredSet htlc_factory_address for that network in config.yaml, or settle via channel
INVALID_ARGUMENT: lock_type (rejected/unsupported)Passed a lock_type a protocol with a single canonical lock construction doesn't accept, or an HtlcExpectation.lock_type the lock doesn't matchLeave lock_type empty for the node default, or pin the type the counterparty actually used
FAILED_PRECONDITION from preimage.SettleHtlcPreimageCalled it on a network with no on-chain HTLC facetUse preimage.SettlePreimage alone there — that network settles through channels only
On-chain HTLCs not swept after preimage.SettlePreimageSince 2026-08-01 SettlePreimage covers channel legs onlyAlso call preimage.SettleHtlcPreimage; on a network with both facets you drive both
SettleHtlcPreimage returns an empty txids listThe preimage unlocks no Locked HTLC on that network (or they were already claimed)Not an error — it is idempotent and a no-op is a success. A partial list is also normal: a claim that can't be built is skipped, not failed

Liquidity Service (Lease API)

(2026-10-14) A request the liquidity service refuses with INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS, FAILED_PRECONDITION or RESOURCE_EXHAUSTED is answered with that code, the service's words in the message — unless those words name a condition that clears, such as a peer that is not connected, which keeps the code they pick. Before, Hydra App answered most of these refusals with INTERNAL.

ErrorCauseSolution
INVALID_ARGUMENT: lease_duration_seconds requiredserver_amount > 0 without a durationSet lease_duration_seconds, or set server_amount = "0"
INVALID_ARGUMENT: dual_fund_fee_payment not allowedUsed dual_fund_fee_payment or sponsored_deposit_fee_payment outside a channel liquidity request, or sponsored_withdrawal_fee_payment outside a release requestEach sponsored method belongs to one operation — see the per-operation table
ALREADY_EXISTS: client_request_id_taken: payment_uuid=…(2026-10-14) A Submit* call of a different request under a client_request_id that already names one. payment_uuid is the request the id namesOn a retry, resend the first request exactly — a re-estimated quote_id makes it a different request; otherwise use a new id. See client_request_id
INVALID_ARGUMENT: client_request_id must be 1 to 64 characters, got N(2026-10-14) An empty or over-long client_request_idSend 1 to 64 characters, or no id
INVALID_ARGUMENT: client_request_id is answered by Submit…, not by this deprecated RPC(2026-10-14) A client_request_id on a deprecated Request* RPC, which refuses it; the message names the matching Submit* RPCCall that Submit* RPC
NOT_FOUND: no such liquidity request(2026-10-14) GetLiquidityRequest for a request your node does not hold on that networkCheck the network and the id. Right after a Submit* call was cut off, submit again under the same id instead — the first call may not have reached the service yet
DEADLINE_EXCEEDED on a Submit* call(2026-10-14) The call outlived the 30 s request deadline; the request may already have been madeSubmit again under the same client_request_id, or read the request back by it — see above
Channel holds a balance but no gas, and every fee method is refusedOn-chain needs a funded wallet; off-chain needs the very balance the release is about to take away(2026-09-11) Use sponsored_withdrawal_fee_payment — the only method that works from that state
FAILED_PRECONDITION naming a quote_id(2026-09-11) The quote expired, or the request no longer matches the one it was issued forRe-run the matching Estimate*Fee, show the user the new fee, and send the fresh quote_id — see Quotes
-32602 unknown field 'assetIds' on a withdraw releaseChannelReleaseOperation.Withdraw replaced asset_ids[] with asset_amounts{} (2026-07-23)Send a map of asset_id → { server_amount?, client_amount? }; at least one side per asset — see ChannelReleaseOperation
INVALID_ARGUMENT on a withdraw with no channel_idchannel_id may be absent only on EstimateRequestChannelReleaseFee, and every side must then be an exact amountSupply channel_id for the real operation
A release quote comes back for a channel you don't own, or the exit falls to the local rail(2026-09-13) Sent channel_id: "" where the field should be absent. The service's ownership check rejects an empty stringOmit the field on an estimate — see ChannelReleaseOperation
INVALID_ARGUMENT: exactly one fee_payment requiredNone or multiple fee_payment variants setSet exactly one
RESOURCE_EXHAUSTED: Hub capital committed to this client would exceed the per-client limit…The request would take your leased capital past the provider's per-client limitLease less, or let a lease lapse first
RESOURCE_EXHAUSTED naming a sponsored rail's budgetThe provider has absorbed as much unrecovered gas, or runs as many sponsored rounds, as it allows for nowTry again later, or pay the fee on another rail
FAILED_PRECONDITION: A fee is not accepted in … on the … rail(2026-10-14) The fee's asset or rail is not among those the provider acceptsPay in an asset, and on a rail, listed in GetLiquidityServiceInfo's fee_payment_assets
FAILED_PRECONDITION: Operation #N: asset … of channel … has an on-chain operation awaiting confirmation(2026-10-14) A deposit, withdrawal or close of an asset channel whose last on-chain operation has not confirmedWait until your node shows the asset channel is_updatable again, then ask again — do not retry on a timer
FAILED_PRECONDITION: Operation #N: asset … of channel … is not updatable(2026-10-14) A deposit, withdrawal or close of an asset channel the hub's node cannot update now: an update of it is under way, or your node is not connected to the hubWait until your node shows the asset channel is_updatable again — reconnect to the hub if your node is not connected — then ask again; do not retry on a timer
FAILED_PRECONDITION: channel does not existBad channel_id on Release / Lease ExtensionVerify with watchOnlyNode.GetChannel
FAILED_PRECONDITION: … the hub keeps asset … of channel … open until …(2026-10-14) A withdraw or cooperative_close of an asset channel the hub opened on demand, before the hub's keep_open_until, the time the refusal names. Refused by the estimate and the request alike, before any fee is paidClose the channel from your own node, or release it once that time has passed — see Rental

Watch-Only Node Service

ErrorCauseSolution
NOT_FOUND: channel not foundBad channel_idUse GetChannels to enumerate
NOT_FOUND: payment not foundBad payment_idUse GetPayments / GetPaymentsByHash to look up

Client Service

ErrorCauseSolution
INVALID_ARGUMENT: invalid amountAmount oneof unset, or DecimalString not a numberSet exact or all; ensure value parses
FAILED_PRECONDITION: insufficient onchain balanceWallet under-funded for the send + feeWait for incoming deposits or reduce amount
FAILED_PRECONDITION: token allowance too lowEVM token send needs higher allowanceCall client.SetTokenAllowance first, or submit a signed token permit
FAILED_PRECONDITION on a token operation with a funded token balanceThe native asset cannot cover the gas — a separate resource from the token being movedFund the native asset, or use a rail that pays it for you: SponsoredDepositFeePayment / DEPOSIT_RAIL_LIQUIDITY_SERVICE
The permit submission reverted on-chainThe deadline passed, the signature does not match owner, or the token already consumed that nonceRe-read blockchain.GetTokenPermitTerms, re-sign, and submit promptly. The fee is spent either way

Pricing Service

ErrorCauseSolution
No price returnedThe oracle does not list that deployment or its symbol, or the price it holds is staleThe response field price is simply absent — a success with an empty optional, not an error. Treat it as "unknown" and render without a fiat figure

Idempotency & Retry Safety

A bot must know which RPCs are safe to retry blindly after a DEADLINE_EXCEEDED or transient UNAVAILABLE.

Safe to retry (read-only, idempotent)

RPC familySafe to retry?
All Get* queries on every service✅ Always
Estimate* RPCs (e.g. EstimateOpenChannelFee, EstimateRequestChannelLiquidityFee)✅ Always
Subscribe* (re-establishing after a drop)✅ Always — reconnect with backoff
preimage.SettlePreimage✅ Idempotent — settling an already-settled preimage is a no-op
preimage.SettleHtlcPreimage✅ Idempotent — an HTLC already claimed is no longer Locked, so a retry simply claims nothing (empty txids)
node.AddPeerToBlacklist / RemovePeerFromBlacklist✅ Idempotent
htlc.UnwatchHtlc✅ Idempotent
liquidity.SubmitChannelLiquidityRequest, SubmitChannelReleaseRequest, SubmitChannelLeaseExtensionRequest — sent again unchanged under the same client_request_id✅ (2026-10-14) Answers with the request already made, as it stands now, and pays nothing again. Without an id they are not idempotent — see below

Retry only after checking server state first

These RPCs are not idempotent. After a DEADLINE_EXCEEDED you may have triggered the action without seeing the response. Verify before retrying:

RPCHow to verify before retry
client.SendTransaction, client.SendTokenTransaction, client.BumpTransactionQuery wallet.GetTransactions for a recent tx with the same recipient / amount
client.SetTokenAllowanceQuery blockchain.GetTokenAllowance for the new value
client.FinalizeAndBroadcastTransactionQuery wallet.GetTransaction for the txid
node.OpenChannel, node.DepositChannel, node.WithdrawChannel, node.UpdateChannel, node.CloseChannel, node.ForceCloseChannel, node.RedeemClosedChannelWatch SubscribeNodeEvents — events for the channel will fire if the operation went through
node.SendChannelPayment, node.SendPayment, node.PayInvoice, node.PayEmptyInvoiceQuery watchOnlyNode.GetPaymentsByHash (you must know the payment hash)
liquidity.RequestChannelLiquidity, liquidity.RequestChannelRelease, liquidity.RequestChannelLeaseExtension (deprecated), and the liquidity.Submit* RPCs sent without a client_request_id(2026-10-14) Your node records every request these make. Find it on liquidity.SubscribeLiquidityRequests, whose opening snapshot holds every request still in progress, or in liquidity.GetFeePayments, then follow it by its payment_uuid with liquidity.GetLiquidityRequest. Better: use the Submit* RPCs under a client_request_id, and retry under it
orderbook.CreateOrderDon't — set client_order_id instead and the retry is idempotent by construction. Without one, query orderbook.GetAllOwnOrders for the same (base, quote, side, amount, price) shape, which is not perfectly unique
orderbook.CancelOrderQuery orderbook.GetOrder — if status is cancelled, the request succeeded
swap.Swap, swap.SimpleSwapswap.SubscribeSimpleSwaps will emit progress; otherwise orderbook.GetAllOwnOrders

Never auto-retry

StatusWhy
INVALID_ARGUMENTYour request is wrong — fix the code, don't retry
FAILED_PRECONDITIONState doesn't allow this — wait for state change, then re-evaluate (don't blindly retry)
PERMISSION_DENIED / UNAUTHENTICATEDAuth issue — re-authenticate first
OUT_OF_RANGENumerical input outside permitted range — fix
UNIMPLEMENTEDRPC doesn't exist on this server version

Application-level idempotency keys

orderbook.CreateOrder takes an idempotency token. Set client_order_id (your own string, max 64 chars) and it names that one order, while the order lives and for a retention period after it ends (24 hours by default). A retry of the same order under it gets the original answer — its order_id, and what its placement released — even when the order has ended meanwhile, instead of placing a second order; a different order under it is refused with client_order_id_taken. Read an open order back with GetOrderByClientId. Use it on every retry-prone order path — it is strictly better than any verify-then-retry dance.

(2026-10-14) So do the three liquidity.Submit* RPCs. Set client_request_id (your own string, 1 to 64 characters) and it names that one request on that network, across restarts of your node. The same request sent again under it gets the request already made, as it stands now, and pays nothing; a different request under it is refused with ALREADY_EXISTS and client_request_id_taken: payment_uuid=…, naming the request the id holds. A request the service refused leaves the id free. Read a request back by it with GetLiquidityRequest. See client_request_id.

Everything else needs an action ID at the application level:

  1. Generate a UUID for each high-level action (e.g. "place 0.1 BTC sell at $67k").
  2. Persist (action_id, status: pending|done|failed) on disk before calling the RPC.
  3. After the RPC returns or after a verification check, update the status.
  4. On startup, replay any pending actions: query the relevant Get* RPC, decide if the action completed, and update.

For order placement, the simplest correct scheme is to make client_order_id be your action_id: replaying a pending action then converges on the same order rather than creating a second one. (2026-10-14) For a liquidity request, make client_request_id your action_id the same way.


Debugging Tips

  1. Enable verbose logging - Log all requests and responses
  2. Check error.message - Contains detailed error information
  3. Verify request format - Use protobuf debugging tools
  4. Test with small amounts - Use minimal values when debugging
  5. Check network status - Ensure blockchain is synced
  6. Monitor rate limits - Avoid excessive requests

← Back to Setup Guide | Next: JSON-RPC →


Copyright © 2025