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
| Method | Path | Auth |
|---|---|---|
GET | /api/v1/tokens | Public |
GET | /api/v1/assets | Signed |
GET | /api/v1/assets/:tokenCode | Signed |
GET | /api/v1/transaction-histories | Signed |
POST | /api/v1/balances/convert/estimate | Signed |
POST | /api/v1/balances/convert | Signed |
POST | /api/v1/balances/transfer | Signed |
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. Forper_wallet, there is one row per token and depositwalletId.
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
| Param | Type | Description |
|---|---|---|
network | string | Optional. 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
}
}
]
}| Field | Description |
|---|---|
id | Allowed token UUID |
tokenCode | Unique token code (for example usdt-ethereum) |
symbol | Display symbol (for example USDT) |
decimals | On-chain decimals |
tokenAddress | Token contract address on that network |
network.id | Network UUID |
network.code | Network code used elsewhere in the API (ethereum, polygon-pos, tron, …) |
network.type | Signing family: evm, tron, solana, or bitcoin |
network.chainId | Chain 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:
| Field | Description |
|---|---|
pendingBalance | Off-chain. Incoming amount detected but not yet finalized into your spendable balance. |
activeBalance | Off-chain. Spendable balance. Withdrawals check and deduct from this. |
withholdBalance | Off-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
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page |
walletId | string | — | 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.
| Field | Balance type | Description |
|---|---|---|
walletId | — | Deposit wallet UUID (per_wallet only) |
pendingBalance | Off-chain | Incoming, not yet spendable |
activeBalance | Off-chain | Spendable balance |
withholdBalance | Off-chain | Reserved 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.
| Code | Meaning |
|---|---|
| 404 | Asset/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
| Param | Type | Description |
|---|---|---|
page | int | Default 1 |
limit | int | Default 20 |
startDate | string | RFC3339 or YYYY-MM-DD |
endDate | string | RFC3339 or YYYY-MM-DD |
transactionType | string | deposit, withdraw, withdraw_fee, withdraw_profit_fee |
method | string | credit or debit |
tokenCode | string | Filter 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
}
}| Field | Type | Description |
|---|---|---|
id | string | Ledger row UUID |
tokenCode | string | Allowed token code |
network | string | Network code |
symbol | string | Display symbol |
method | string | credit (balance increased) or debit (balance decreased) |
transactionType | string | deposit, withdraw, withdraw_fee, or withdraw_profit_fee |
amount | number | Off-chain movement amount in token units |
latestBalance | number | Off-chain activeBalance after this entry |
referenceType | string | Source entity: invoice, deposit, token_transfer, or withdraw |
referenceId | string | UUID of the source entity |
createdAt | string (RFC3339) | When the ledger row was written |
| Code | Meaning |
|---|---|
| 400 | Invalid date range, transactionType, or method |
| 404 | Token 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:
- Estimate —
POST /api/v1/balances/convert/estimate(quote only; no balance change) - Convert —
POST /api/v1/balances/convert(applies the same quote rules and updates balances)
Rules that apply to both endpoints
| Rule | Detail |
|---|---|
| Auth | Requires authentication |
| Balance type | per_app only. per_wallet apps get an error |
| Tokens | from and to must be different allowed token codes (from GET /api/v1/tokens), both active |
| Peg currencies | Each token must have a configured peg (e.g. USDT → USD, XSGD → SGD) |
| Same peg | If 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 peg | If pegs differ (e.g. USD → SGD), Xellar fetches an FX rate, applies the source token’s convert markup, then credits toAmount |
| Rounding | Amounts 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 × effectiveRateExample (cross peg): convert 100 USDT (USD) → XSGD (SGD), market rate 1.35, markup 1%:
rate=1.35effectiveRate=1.35 × (1 − 0.01)=1.3365toAmount=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
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source allowed token code to debit. Must exist and be active. Example: usdt-ethereum |
to | string | Yes | Destination allowed token code to credit. Must exist, be active, and differ from from. Example: xsgd-ethereum |
amount | number | Yes | Amount 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)
| Field | Type | Description |
|---|---|---|
from | string | Source allowed token code (echo of request) |
to | string | Destination allowed token code (echo of request) |
amount | number | Source amount after rounding — how much will be debited from from |
toAmount | number | Destination amount after markup — how much will be credited to to |
rate | number | Raw FX rate from peggedFrom → peggedTo before markup. 1 when samePeg is true |
effectiveRate | number | Rate after markup: rate × (1 − markupPercentage / 100). 1 when samePeg is true |
markupPercentage | number | Convert markup % taken from the source token config. Forced to 0 when samePeg is true |
peggedFrom | string | Fiat/currency peg of the source token (uppercase), e.g. USD |
peggedTo | string | Fiat/currency peg of the destination token (uppercase), e.g. SGD |
fxProvider | string | FX provider used for a cross-peg quote (e.g. frankfurter). Empty string when samePeg is true |
samePeg | boolean | true 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)
| Code | Typical error message | When |
|---|---|---|
| 400 | amount must be greater than zero | amount ≤ 0 |
| 400 | from and to tokens must be different | Same token code for from and to |
| 400 | token is not active | Token exists but inactive |
| 400 | balance convert is only allowed for per_app apps | App is per_wallet |
| 400 | both tokens must have pegged_to set | Missing peg on either token |
| 400 | from token must have fx_provider set for cross-peg convert | Cross-peg convert but source has no FX provider |
| 400 | unsupported fx provider | Source token FX provider is not supported |
| 400 | convert markup percentage is invalid | Markup config is invalid (< 0 or effective rate ≤ 0) |
| 404 | token not found | Unknown from or to token code |
| 502 | fx rate unavailable | FX 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
amountfrom the source token’s off-chainactiveBalance - Credits
toAmountto the destination token’s off-chainactiveBalance - Records the conversion (rate, markup, pegs, balances after)
Request body is identical to estimate. Unlike estimate, convert requires sufficient source activeBalance.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source allowed token code to debit. Must have enough activeBalance |
to | string | Yes | Destination allowed token code to credit (must differ from from) |
amount | number | Yes | Source 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
| Field | Type | Description |
|---|---|---|
id | string | UUID of the stored conversion record |
from | string | Source token code |
to | string | Destination token code |
amount | number | Amount debited from source |
toAmount | number | Amount credited to destination |
rate | number | Raw FX rate before markup (1 if same peg) |
effectiveRate | number | Rate after markup (1 if same peg) |
markupPercentage | number | Markup % applied (0 if same peg) |
peggedFrom | string | Source peg currency |
peggedTo | string | Destination peg currency |
fxProvider | string | FX provider used (empty if same peg) |
samePeg | boolean | Whether conversion was 1:1 same-peg |
fromBalance | object | Source asset ledger after debit — see below |
toBalance | object | Destination asset ledger after credit — same shape |
Quote fields (from … samePeg) match the estimate quote object.
Asset object (fromBalance / toBalance)
Same shape as List Assets:
| Field | Type | Description |
|---|---|---|
tokenCode | string | Allowed token code |
network | string | Network code (e.g. ethereum) |
networkType | string | evm, tron, solana, or bitcoin |
chainId | number | Chain id (0 when not applicable) |
tokenAddress | string | Token contract address |
symbol | string | Display symbol |
decimals | number | On-chain decimals |
pendingBalance | number | Off-chain. Not yet spendable |
activeBalance | number | Off-chain. Spendable balance after this conversion |
withholdBalance | number | Off-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:
| Token | Before | After |
|---|---|---|
usdt-ethereum (from) | 100 | 0 (debited amount = 100) |
xsgd-ethereum (to) | 0 | 133.65 (credited toAmount) |
Errors (convert)
Same as estimate errors, plus:
| Code | Typical error message | When |
|---|---|---|
| 400 | insufficient active balance: you have X, need Y | Source 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
| Field | Type | Required | Description |
|---|---|---|---|
destinationAppId | string | Yes | Target app UUID (must not equal your X-App-ID) |
amount | number | Yes | Amount to transfer (> 0) |
token | string | Yes | Allowed token code |
sourceWalletId | string | Conditional | Required when your app is per_wallet — ledger wallet to debit |
walletId | string | Conditional | Required 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
| Field | Type | Description |
|---|---|---|
id | string | Transfer record UUID |
sourceAppId | string | Your app (from X-App-ID) |
destinationAppId | string | Credited app |
token | string | Token code transferred |
amount | number | Amount moved |
sourceWalletId | string | omitted | Source ledger wallet (per_wallet source) |
walletId | string | omitted | Destination ledger wallet (per_wallet destination) |
sourceActiveBalance | number | Source off-chain activeBalance after transfer |
destActiveBalance | number | Destination off-chain activeBalance after transfer |
| Code | Meaning |
|---|---|
| 400 | Validation, wallet rules, insufficient balance |
| 404 | Destination app or token not found |