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 asset id",
  "details": []
}

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.

Three LiquidityService RPCs are exempt from that cap — RequestChannelLiquidity, RequestChannelRelease and RequestChannelLeaseExtension. Each waits for an on-chain channel operation to be observed, which outlives 30 s on every supported network; they rely on their own internal deadlines instead. Set a client deadline generous enough for a confirmation on those three, or you will abandon a request whose fee has already been paid.

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.


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)

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: Resource has been exhausted

Common causes:

  • Insufficient balance
  • No liquidity available
  • Lease capacity exceeded
  • Channel capacity full

Examples:

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

// Error: no liquidity
{
  "code": 8,
  "message": "no liquidity available in orderbook"
}

// Error: lease capacity
{
  "code": 8,
  "message": "lease amount exceeds available liquidity"
}

Action:

  • Check balance before operations
  • Add funds to wallet
  • Try smaller amount
  • Wait for liquidity to become available

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

Examples:

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

// 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

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
  • Endpoint deprecated
  • Network not supported

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:
      if (error.message.includes('balance')) {
        return 'Insufficient funds. Please add more to your wallet.'
      }
      if (error.message.includes('liquidity')) {
        return 'Not enough liquidity available. Try a smaller amount.'
      }
      return 'Resource limit exceeded.'

    case 9:
      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
RESOURCE_EXHAUSTED: 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
RESOURCE_EXHAUSTED: 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
RESOURCE_EXHAUSTED: insufficient capacityChannel too smallDeposit more funds via node.DepositChannel or liquidity.RequestChannelLiquidity (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])
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. The order may already have been announced on the event stream, and ended there. Not a fault of the order: retry, and it matches other liquidity. Hydra App answers it with UNAVAILABLE

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.

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)

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 RequestChannelLiquidity, or sponsored_withdrawal_fee_payment outside RequestChannelReleaseEach sponsored method belongs to one operation — see the per-operation table
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: insufficient liquidityProvider has no inventory for that assetReduce server_amount, try a different asset, or self-fund via node.OpenChannel
FAILED_PRECONDITION: channel does not existBad channel_id on Release / Lease ExtensionVerify with watchOnlyNode.GetChannel

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
RESOURCE_EXHAUSTED: 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
RESOURCE_EXHAUSTED 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

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.RequestChannelLiquidityWatch SubscribeNodeEvents for new channel creation
liquidity.RequestChannelReleaseWatch SubscribeNodeEvents for the channel state change
liquidity.RequestChannelLeaseExtensionQuery watchOnlyNode.GetChannel and check the new expiry
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

One RPC does take an idempotency token: orderbook.CreateOrder. 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.

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.


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