Skip to main content
The things that trip up a first integration on Stellar, in one place. Each one was checked against the live API on mainnet.

Amounts are integer strings in the smallest unit

Every amount you send to the API is an integer number of the asset’s smallest unit (stroops for XLM), written as a string: "100000000" is 10 XLM. Convert from what the user types with raw = human × 10^decimals, using integer or decimal arithmetic, never floats. Every classic Stellar asset has 7 decimals. Tokens issued directly on Soroban choose their own: the Soroswap token list has some with 18. Read decimals from GET /asset-list?name=SOROSWAP or from the token contract’s decimals() instead of assuming 7.
  • "0" and negative amounts return 400 amount must be a positive integer number of stroops.
  • An amount with a decimal point ("10.5") currently returns 500, not 400. Never send one.
  • A JSON number is accepted, but a string is the only form that survives values above 2^53 in JavaScript.
Not every field is in stroops. Read the unit of each response field before you display it:

Assets are contract addresses

The API identifies every asset, classic or Soroban, by its contract address (C...). The CODE:ISSUER form and the issuer’s G... address are not accepted:
  • "assetIn": "USDC:GA5Z..." returns 400 Invalid Stellar address.
  • "assetIn": "GA5Z..." (the issuer) returns 503 Routing unavailable, which reads like an outage but is a wrong input. Check your addresses before you retry it.
A classic asset reaches Soroban through its Stellar Asset Contract (SAC), whose address is derived from the asset and the network. To get it:
The same asset has a different contract address on testnet: XLM is CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC there. Never reuse mainnet addresses with ?network=testnet. Accounts are a separate question. from and to are the wallets that sign and receive: a classic account (G...) or a contract account such as a smart wallet (C...). See Smart wallets below.

A missing trustline

A classic account cannot receive a classic asset (anything but XLM) until it has a trustline for it. Tokens issued on Soroban need no trustline. When the receiving account lacks one:
  • POST /quote/build returns 428 with action: "CREATE_TRUSTLINE". actionData.xdr is a changeTrust transaction for actionData.signer, with assetCode and assetIssuer. Sign it, submit it, then call /quote/build again with the same quote (quote again if it is no longer fresh).
  • POST /swap returns 200 with the same action instead of an error, so one call carries the prerequisite. With inlinePrerequisites: true it also returns the swap XDR, with the trustline transaction in a prerequisites array to submit first.
  • On a mixed-venue route the missing trustline can be for an intermediate asset, not the output one. Read assetCode from the response instead of assuming it is assetOut.
  • A user with no XLM to pay for the trustline can still receive the asset in one swap through Gasless Trustline, on SDEX routes, with a sponsor account you control.

The XLM reserve

Each account keeps a minimum XLM balance it cannot spend: 0.5 XLM per base reserve, two for the account itself plus one per trustline, offer or other subentry. Opening a trustline locks another 0.5 XLM. A user who swaps “all” their XLM fails: size XLM swaps from what is spendable. GET /balances/{address} returns it as available / availableRaw, already net of the reserve.

A built transaction expires

The XDR from /quote/build and /swap has a time bound. Past it, the network rejects the transaction with tx_too_late. The quote inside it ages faster than that: prices move and the minimum output was set at quote time. If the user has not signed within the window, quote and build again. Never re-sign or resubmit an expired XDR, and never edit its time bounds.

Simulation: the API has done it

A Soroban transaction must be simulated before it is signed, to get its footprint, resource fee and authorization entries. /quote/build already did that and sized the result for the route. Sign it and send it as is. Do not run prepareTransaction or another simulation on it: simulating again drops the margins the API adds for Aquarius and SushiSwap routes, and the swap can fail on chain. The one exception is a swap XDR that comes back with simulated: false from /swap with inlinePrerequisites: true. It could not be simulated because its prerequisites did not exist yet: submit them first, then simulate and assemble it yourself. A simulation that fails while building returns 400 Simulation Failed with a contract errorCode, for example 10 (TokenError.InsufficientBalance): the account cannot cover the amount. Fix the account state and build again.

Signing: envelope, not auth entries

The XDR the API builds uses the from account as transaction source, and its Soroban authorization is source-account credentials. A classic wallet (Freighter, Stellar Wallets Kit, a keypair on your server) signs the transaction envelope once and that covers everything. You do not sign authorization entries separately. The one exception to a single signer is Gasless Trustline, where the sponsor signs too.

Smart wallets and passkeys

A contract account (C...), such as a passkey smart wallet, cannot be the source of a transaction, so it cannot use /quote/build: a C... in from returns 400 invalid version byte. expected 48, got 16. Smart wallets take the contract path:
  1. Quote with /quote, leaving sdex out of protocols (the classic DEX cannot pay a contract; /swap drops it for you and returns 400 if it was the only venue).
  2. Build the aggregator swap_exact_in / swap_exact_out invocation yourself from rawTrade.distribution.
  3. Simulate it with a fee-paying G... account as source, have the smart wallet sign the authorization entry for its own address, then assemble and submit.
Soroban routes deliver to the signing account only: on /quote/build, to must equal from or be left out.

Testnet is a different world

?network=testnet uses the same base URL and key. Testnet quotes come only from Soroswap pools, carry no fee and use testnet contract addresses. Fund an account with Friendbot and get test tokens at faucet.soroswap.finance. Leaving out ?network quotes mainnet, so always send it.

Responses you might misread

  • A successful POST returns 201, not 200. Treat any 2xx as success.
  • A missing or invalid key returns 403 Forbidden resource, never 401. See Errors.