Conduitdocs

Error codes

Every error response has the shape:

json
{
  "error": {
    "type": "conduit_error",
    "code": "fx_quote_expired",
    "message": "The FX quote has expired. Request a new quote.",
    "param": "quote_id",
    "doc_url": "https://docs.conduit.xyz/errors/fx_quote_expired"
  }
}

internal/errors/registry.go is the single source of truth on the server side. No raw contract revert string or upstream StableFX error body ever reaches a client — every failure is translated through this registry first.

CodeHTTP statusMeaningWhat to do
fx_quote_expired409The quote you tried to /prepare against is past its quote_expires_at.Request a fresh /quote — quotes are disposable, never cached (see FX timing model).
fx_no_route422StableFX doesn't quote this currency pair.Check GET /v1/currencies for what's actually routable right now — it reflects live StableFX coverage, not a static list.
fx_invalid_amount422The amount is outside StableFX's quotable range (too small or too large).There's a real minimum notional (observed ~1 unit of a major currency on the sandbox) — don't assume any amount ≥ 1 minor unit is quotable.
fx_provider_unavailable503StableFX returned an error code this API doesn't have a specific mapping for.Transient — retry with backoff. If it persists, it's worth a bug report since it means a new upstream error code needs mapping.
currency_not_supported422The currency isn't registered in CurrencyRegistry at all.Check GET /v1/currencies.
intent_expired409The settlement intent's expires_at has passed.Create a new intent.
intent_already_settled409You tried to quote/prepare/confirm/cancel an intent that's already settled.Fetch the intent (GET /v1/settlement_intents/:id) — it already succeeded.
idempotency_key_reuse409You reused an Idempotency-Key with a request body that hashes differently from the first use.Use a new key for a genuinely different request; reuse the same key only to safely retry the exact same request.
signature_invalid400The EIP-712 signature didn't verify against the expected signer/typed data.Check you're signing the exact typed_data/funding_typed_data object the API returned, unmodified.
insufficient_payer_balance422The payer doesn't hold enough of the pay currency to fund this payment.Nothing the API can do here — surface this to the payer directly.
invalid_request400Malformed request body or missing required field.param on the error names the offending field.
not_found404The resource doesn't exist, or exists but isn't visible to this API key (wrong account, wrong livemode).Double-check the ID and that your key's livemode matches.
unauthorized401Missing or invalid API key.Check the Authorization: Bearer header.
forbidden403Your key is valid but isn't allowed to do this — e.g. a cross-tenant Conduit-Account header.Use the right account.
internal_error500Something broke on our end.Retry with backoff; if it persists, report it — this code intentionally carries no detail because a raw internal error should never leak to a client.
settle_address_derived400You sent settle_address on a request that derives it from the account.Remove the field. Where a payment settles comes from the account that owns it — see settlement addresses. Refused rather than ignored, so an integration cannot keep sending an address and keep being paid somewhere else.
settlement_wallet_required409This business has no settlement wallet of its own yet.It is provisioned automatically on the next dashboard visit. Until then nothing is created, because the alternative is a link quietly paying into somebody's personal wallet.
settlement_wallet_unknown403The wallet id given does not belong to the signed-in Circle user.Only a wallet Circle lists for that user can back a settlement address. Deliberately not a 404 — saying which ids exist would let a caller enumerate other people's wallets.
settlement_wallet_invalid422That wallet cannot be used for settlement — wrong chain, or it is the sign-in wallet.Conduit settles on Arc, and the sign-in wallet is the thing being moved away from.
settlement_wallet_already_set409This account already has a settlement wallet.Moving where income lands is a deliberate act with its own confirmation — see Advanced in settlement addresses.
payout_challenge_required409You tried to verify a payout destination with no outstanding challenge.Request one first: POST /v1/payout_destinations/{id}/challenge.
payout_challenge_expired409That challenge is no longer valid — expired, answered, or replaced.Request a fresh one. Nonces are single-use, so an expired one cannot simply be signed again.
payout_destination_unverified409Nobody has proven control of that address.Sign the challenge with the wallet at that address. An unproven address is indistinguishable from a typo, and the transfer is final.
payout_not_found_on_chain422The transaction given does not contain the transfer this payout describes.Check the hash. A ledger built from what a client says happened is a ledger that can be told anything.
confirmation_mismatch400The confirmation text did not match the account name.Type it exactly. This is friction rather than security — it exists so the change cannot happen by mis-clicking.
upstream_unavailable503A service this request depends on could not be reached.Retry with backoff. Distinct from a definite refusal on purpose: retrying is right for one and not the other.
username_needs_a_person422A username belongs to a person.The caller is a company with nobody behind it — an API key rather than somebody signed in. A username resolves to a person's own address, so there is nothing sensible to bind it to.
employee_already_exists409Somebody on this payroll is already paid at that address.Two rows for one address is how a person gets paid twice in a single run. Edit the existing one.
payroll_no_employees422There is nobody active to pay.Add employees, or un-pause them. Refused rather than producing an empty run somebody then tries to execute.
payroll_run_not_draft409This run has already been executed.Build a new one. A run is executed once.
payroll_run_key_reused409That run key has already been used. Nobody was paid twice.The response carries the run the key belongs to. This is the refusal that makes a double-clicked payroll button harmless.
payroll_not_configured503Payroll is not configured on this deployment.CONDUIT_PAYROLL_ADDRESS names no contract. The API ships with the deployed Arc testnet address built in, so this now means the variable was set to something empty or wrong rather than simply left alone.
payroll_insufficient_balance422This payroll costs more than the settlement wallet holds.The response carries balance, required, short_by and currency. Refused before anything is signed: a payroll that starts short pays the first currency group, empties the wallet, and leaves the rest unpaid — an outcome nobody chose.
payroll_leg_not_found_on_chain422That transaction does not contain this group's payment.Recording a payment that did not happen would tell everybody in the group they had been paid, so the transaction is checked against the contract's own event.

rate_limited (429)

The public payer routes are rate limited per client: 5 requests/second sustained, burst 20. Loading a pay page, polling its status, and requesting a quote sit comfortably inside that; a script looping over a link URL does not.

The response carries Retry-After: 1. Back off and retry rather than tightening the loop — the budget refills continuously, so a client that waits recovers within a second.

Authenticated routes (sk_ keys, dashboard sessions) are limited separately, per account: a key is already a credential and can be revoked.