Custodian
Assets & Balances

Assets & Balances

Read the public token catalog (no auth), your off-chain balances, ledger history, and optional convert / transfer operations. Balance endpoints require authentication.

Signed endpoints on this page

MethodPathAuth
GET/api/v1/tokensPublic
GET/api/v1/assetsSigned
GET/api/v1/assets/:tokenCodeSigned
GET/api/v1/transaction-historiesSigned
POST/api/v1/balances/convert/estimateSigned
POST/api/v1/balances/convertSigned
POST/api/v1/balances/transferSigned

Balance type: Everything on this page except List Allowed Tokens is off-chain — your app's ledger balance at Xellar, not a live blockchain read. See Understanding Balances.

For per_app (default), there is one row per token. For per_wallet, there is one row per token and deposit walletId.

List Allowed Tokens (Public)

Returns every active allowed token and its active network. No authentication. Use this to populate token/network pickers for invoices, withdraws, and listen flows.

The payload is catalog data only: token id, code, symbol, decimals, contract address, and network id/code/type/chainId. It does not include RPC URLs, credentials, or other infrastructure fields.

GET /api/v1/tokens

Query Parameters

ParamTypeDescription
networkstringOptional. Filter by network code (for example ethereum, tron)

Response 200

{
  "data": [
    {
      "id": "tok_abc123",
      "tokenCode": "usdt-ethereum",
      "symbol": "USDT",
      "decimals": 6,
      "tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
      "network": {
        "id": "net_abc123",
        "code": "ethereum",
        "type": "evm",
        "chainId": 1
      }
    }
  ]
}
FieldDescription
idAllowed token UUID
tokenCodeUnique token code (for example usdt-ethereum)
symbolDisplay symbol (for example USDT)
decimalsOn-chain decimals
tokenAddressToken contract address on that network
network.idNetwork UUID
network.codeNetwork code used elsewhere in the API (ethereum, polygon-pos, tron, …)
network.typeSigning family: evm, tron, solana, or bitcoin
network.chainIdChain id (EVM/TRON); 0 when not applicable

Inactive tokens and tokens on inactive networks are omitted.

Off-Chain Balance Fields

Each token asset has three balance buckets:

FieldDescription
pendingBalanceOff-chain. Incoming amount detected but not yet finalized into your spendable balance.
activeBalanceOff-chain. Spendable balance. Withdrawals check and deduct from this.
withholdBalanceOff-chain. Amount temporarily held while a withdrawal is in progress.

latestBalance in transaction history is also off-chain — it is a snapshot of activeBalance after each ledger entry.

List Assets

GET /api/v1/assets

Returns off-chain balances per token.

Query Parameters

ParamTypeDefaultDescription
pageint1Page number
limitint20Items per page
walletIdstring—Filter to one deposit wallet (per_wallet apps)

Response 200

{
  "data": {
    "items": [
      {
        "tokenCode": "USDT",
        "network": "ethereum",
        "networkType": "evm",
        "chainId": 1,
        "tokenAddress": "0x...",
        "symbol": "USDT",
        "decimals": 6,
        "walletId": "wal_1",
        "pendingBalance": 0,
        "activeBalance": 100.5,
        "withholdBalance": 0
      }
    ],
    "total": 5,
    "page": 1,
    "limit": 20
  }
}

walletId is present on per_wallet apps and omitted on per_app.

FieldBalance typeDescription
walletId—Deposit wallet UUID (per_wallet only)
pendingBalanceOff-chainIncoming, not yet spendable
activeBalanceOff-chainSpendable balance
withholdBalanceOff-chainReserved for in-flight withdrawals

Get Asset by Token Code

GET /api/v1/assets/:tokenCode → { "data": { /* single asset */ } }

Optional query walletId — required to pick a single row when the app is per_wallet and the token has balances on more than one wallet.

Returns off-chain balance for one token. Response fields match List Assets.

CodeMeaning
404Asset/token not found

List Transaction History

GET /api/v1/transaction-histories

Returns your off-chain ledger — credits and debits that changed your balance over time.

Query Parameters

ParamTypeDescription
pageintDefault 1
limitintDefault 20
startDatestringRFC3339 or YYYY-MM-DD
endDatestringRFC3339 or YYYY-MM-DD
transactionTypestringdeposit, withdraw, withdraw_fee, withdraw_profit_fee
methodstringcredit or debit
tokenCodestringFilter by allowed token code (e.g. usdt-ethereum)

Response 200

{
  "data": {
    "items": [
      {
        "id": "txn_abc123",
        "tokenCode": "USDT",
        "network": "ethereum",
        "symbol": "USDT",
        "method": "credit",
        "transactionType": "deposit",
        "amount": 100,
        "latestBalance": 100.5,
        "referenceType": "invoice",
        "referenceId": "inv_abc123",
        "createdAt": "2026-01-01T00:00:00Z"
      }
    ],
    "total": 20,
    "page": 1,
    "limit": 20
  }
}
FieldTypeDescription
idstringLedger row UUID
tokenCodestringAllowed token code
networkstringNetwork code
symbolstringDisplay symbol
methodstringcredit (balance increased) or debit (balance decreased)
transactionTypestringdeposit, withdraw, withdraw_fee, or withdraw_profit_fee
amountnumberOff-chain movement amount in token units
latestBalancenumberOff-chain activeBalance after this entry
referenceTypestringSource entity: invoice, deposit, token_transfer, or withdraw
referenceIdstringUUID of the source entity
createdAtstring (RFC3339)When the ledger row was written
CodeMeaning
400Invalid date range, transactionType, or method
404Token not found

Balance convert (overview)

Convert moves off-chain balance from one allowed token to another inside the same app. Nothing is sent on-chain.

Recommended flow:

  1. Estimate — POST /api/v1/balances/convert/estimate (quote only; no balance change)
  2. Convert — POST /api/v1/balances/convert (applies the same quote rules and updates balances)

Rules that apply to both endpoints

RuleDetail
AuthRequires authentication
Balance typeper_app only. per_wallet apps get an error
Tokensfrom and to must be different allowed token codes (from GET /api/v1/tokens), both active
Peg currenciesEach token must have a configured peg (e.g. USDT → USD, XSGD → SGD)
Same pegIf both tokens peg to the same currency (e.g. USDT → USD and USDC → USD), conversion is 1:1 (rate = 1, toAmount = amount, no FX markup)
Cross pegIf pegs differ (e.g. USD → SGD), Xellar fetches an FX rate, applies the source token’s convert markup, then credits toAmount
RoundingAmounts and rates are rounded to 8 decimal places

How the quote is calculated

rate            = FX rate from peggedFrom → peggedTo   (or 1 if same peg)
effectiveRate   = rate × (1 − markupPercentage / 100)  (or 1 if same peg)
toAmount        = amount × effectiveRate

Example (cross peg): convert 100 USDT (USD) → XSGD (SGD), market rate 1.35, markup 1%:

  • rate = 1.35
  • effectiveRate = 1.35 × (1 − 0.01) = 1.3365
  • toAmount = 100 × 1.3365 = 133.65
  • Ledger: debit 100 from USDT, credit 133.65 to XSGD

Estimate Convert

POST /api/v1/balances/convert/estimate

Returns a conversion quote without changing balances. Use this to show the user how much they will receive before calling convert.

Does not check whether you have enough activeBalance (insufficient balance is only enforced on convert).

Request body

FieldTypeRequiredDescription
fromstringYesSource allowed token code to debit. Must exist and be active. Example: usdt-ethereum
tostringYesDestination allowed token code to credit. Must exist, be active, and differ from from. Example: xsgd-ethereum
amountnumberYesAmount of the source token to convert (> 0). Rounded to 8 decimals
{
  "from": "usdt-ethereum",
  "to": "xsgd-ethereum",
  "amount": 100
}

Response 200

{
  "data": {
    "from": "usdt-ethereum",
    "to": "xsgd-ethereum",
    "amount": 100,
    "toAmount": 133.65,
    "rate": 1.35,
    "effectiveRate": 1.3365,
    "markupPercentage": 1,
    "peggedFrom": "USD",
    "peggedTo": "SGD",
    "fxProvider": "frankfurter",
    "samePeg": false
  }
}

Response data fields (quote object)

FieldTypeDescription
fromstringSource allowed token code (echo of request)
tostringDestination allowed token code (echo of request)
amountnumberSource amount after rounding — how much will be debited from from
toAmountnumberDestination amount after markup — how much will be credited to to
ratenumberRaw FX rate from peggedFrom → peggedTo before markup. 1 when samePeg is true
effectiveRatenumberRate after markup: rate × (1 − markupPercentage / 100). 1 when samePeg is true
markupPercentagenumberConvert markup % taken from the source token config. Forced to 0 when samePeg is true
peggedFromstringFiat/currency peg of the source token (uppercase), e.g. USD
peggedTostringFiat/currency peg of the destination token (uppercase), e.g. SGD
fxProviderstringFX provider used for a cross-peg quote (e.g. frankfurter). Empty string when samePeg is true
samePegbooleantrue if both tokens share the same peg → 1:1 conversion with no FX call

Same-peg example

{
  "from": "usdt-ethereum",
  "to": "usdc-ethereum",
  "amount": 50
}
{
  "data": {
    "from": "usdt-ethereum",
    "to": "usdc-ethereum",
    "amount": 50,
    "toAmount": 50,
    "rate": 1,
    "effectiveRate": 1,
    "markupPercentage": 0,
    "peggedFrom": "USD",
    "peggedTo": "USD",
    "fxProvider": "",
    "samePeg": true
  }
}

Errors (estimate)

CodeTypical error messageWhen
400amount must be greater than zeroamount ≤ 0
400from and to tokens must be differentSame token code for from and to
400token is not activeToken exists but inactive
400balance convert is only allowed for per_app appsApp is per_wallet
400both tokens must have pegged_to setMissing peg on either token
400from token must have fx_provider set for cross-peg convertCross-peg convert but source has no FX provider
400unsupported fx providerSource token FX provider is not supported
400convert markup percentage is invalidMarkup config is invalid (< 0 or effective rate ≤ 0)
404token not foundUnknown from or to token code
502fx rate unavailableFX provider could not return a usable rate

Convert Balance

POST /api/v1/balances/convert

Applies a conversion using the same quote rules as Estimate Convert:

  • Debits amount from the source token’s off-chain activeBalance
  • Credits toAmount to the destination token’s off-chain activeBalance
  • Records the conversion (rate, markup, pegs, balances after)

Request body is identical to estimate. Unlike estimate, convert requires sufficient source activeBalance.

Request body

FieldTypeRequiredDescription
fromstringYesSource allowed token code to debit. Must have enough activeBalance
tostringYesDestination allowed token code to credit (must differ from from)
amountnumberYesSource amount to convert (> 0). Rounded to 8 decimals
{
  "from": "usdt-ethereum",
  "to": "xsgd-ethereum",
  "amount": 100
}

Response 200

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "from": "usdt-ethereum",
    "to": "xsgd-ethereum",
    "amount": 100,
    "toAmount": 133.65,
    "rate": 1.35,
    "effectiveRate": 1.3365,
    "markupPercentage": 1,
    "peggedFrom": "USD",
    "peggedTo": "SGD",
    "fxProvider": "frankfurter",
    "samePeg": false,
    "fromBalance": {
      "tokenCode": "usdt-ethereum",
      "network": "ethereum",
      "networkType": "evm",
      "chainId": 1,
      "tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
      "symbol": "USDT",
      "decimals": 6,
      "pendingBalance": 0,
      "activeBalance": 0,
      "withholdBalance": 0
    },
    "toBalance": {
      "tokenCode": "xsgd-ethereum",
      "network": "ethereum",
      "networkType": "evm",
      "chainId": 1,
      "tokenAddress": "0x…",
      "symbol": "XSGD",
      "decimals": 6,
      "pendingBalance": 0,
      "activeBalance": 133.65,
      "withholdBalance": 0
    }
  }
}

Response data fields

FieldTypeDescription
idstringUUID of the stored conversion record
fromstringSource token code
tostringDestination token code
amountnumberAmount debited from source
toAmountnumberAmount credited to destination
ratenumberRaw FX rate before markup (1 if same peg)
effectiveRatenumberRate after markup (1 if same peg)
markupPercentagenumberMarkup % applied (0 if same peg)
peggedFromstringSource peg currency
peggedTostringDestination peg currency
fxProviderstringFX provider used (empty if same peg)
samePegbooleanWhether conversion was 1:1 same-peg
fromBalanceobjectSource asset ledger after debit — see below
toBalanceobjectDestination asset ledger after credit — same shape

Quote fields (from … samePeg) match the estimate quote object.

Asset object (fromBalance / toBalance)

Same shape as List Assets:

FieldTypeDescription
tokenCodestringAllowed token code
networkstringNetwork code (e.g. ethereum)
networkTypestringevm, tron, solana, or bitcoin
chainIdnumberChain id (0 when not applicable)
tokenAddressstringToken contract address
symbolstringDisplay symbol
decimalsnumberOn-chain decimals
pendingBalancenumberOff-chain. Not yet spendable
activeBalancenumberOff-chain. Spendable balance after this conversion
withholdBalancenumberOff-chain. Held for in-flight withdrawals

walletId is omitted for per_app.

Example (before → after, cross peg)

Start: USDT activeBalance = 100, XSGD = 0. Convert 100 USDT → XSGD at effective rate 1.3365:

TokenBeforeAfter
usdt-ethereum (from)1000 (debited amount = 100)
xsgd-ethereum (to)0133.65 (credited toAmount)

Errors (convert)

Same as estimate errors, plus:

CodeTypical error messageWhen
400insufficient active balance: you have X, need YSource activeBalance is too low

Transfer Balance

Move off-chain activeBalance from your app to another app under the same client account (same Panel user). Both apps may be per_app or per_wallet; wallet fields apply when needed.

POST /api/v1/balances/transfer

Request body

FieldTypeRequiredDescription
destinationAppIdstringYesTarget app UUID (must not equal your X-App-ID)
amountnumberYesAmount to transfer (> 0)
tokenstringYesAllowed token code
sourceWalletIdstringConditionalRequired when your app is per_wallet — ledger wallet to debit
walletIdstringConditionalRequired when destination app is per_wallet — ledger wallet to credit
{
  "destinationAppId": "4f83980c-c764-4b5d-9c0c-82e301ad6e45",
  "amount": 10,
  "token": "usdt-ethereum",
  "sourceWalletId": "5326036c-50c3-4d63-8150-69cb253542e3",
  "walletId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response 200

FieldTypeDescription
idstringTransfer record UUID
sourceAppIdstringYour app (from X-App-ID)
destinationAppIdstringCredited app
tokenstringToken code transferred
amountnumberAmount moved
sourceWalletIdstring | omittedSource ledger wallet (per_wallet source)
walletIdstring | omittedDestination ledger wallet (per_wallet destination)
sourceActiveBalancenumberSource off-chain activeBalance after transfer
destActiveBalancenumberDestination off-chain activeBalance after transfer
CodeMeaning
400Validation, wallet rules, insufficient balance
404Destination app or token not found