Use WebSocket for ongoing data updates. Keep a persistent connection and subscribe to market data, orders, fills, and balances where available. Maintain local state from those updates instead of repeatedly polling REST.
Use REST to place and cancel orders, transfer funds, load initial state, fetch data without a WebSocket channel, and reconcile after reconnects or uncertain results.
SPY-USDC with illustrative prices, sizes, and fees. Select an enabled market and its increments from your authenticated market configuration before submitting orders.
1. Connect and authenticate
Use credentials issued for the environment and account you intend to trade.
Market availability and account access can differ between environments. An endpoint being reachable does not establish that a market is enabled for your account.
Create your API key through the authenticated account interface. API keys cannot create or manage other API keys. Keep the full key ID and secret, including their prefixes. Store the secret on the server and exclude it from logs.
A key identifies one account. To trade a subaccount, use a key created for that subaccount; do not add a subaccount override header to an API-key request. An IP allowlist, if configured on the key, must include your client’s egress IP.
REST signature
Send these headers on authenticated requests:
Construct the signed bytes without separators:
?, parameter order, and percent encoding. Exclude the scheme and host. Sign the exact body bytes you send; a bodyless request contributes zero bytes, not null or {}. Do not hex-decode or Base64-decode the secret.
The timestamp may be at most 10 seconds behind or 1 second ahead of server time. Synchronize your clock, and generate a fresh timestamp and signature for each request. A signature does not make a write idempotent.
Python client
This example uses the Python standard library. Save it asspot_client.py. Set ONDO_API_BASE, ONDO_KEY_ID, and ONDO_API_SECRET in your environment. Running the file only reads market configuration and balances.
2. Discover markets and units
GET /v1/markets supports public and authenticated requests. Use an authenticated request for trading: the response reflects account-specific market access and maker/taker fees.
Read result.spot.tradingPairs. A market entry has this shape; these values are illustrative:
Use decimal arithmetic for prices, sizes, balances, and fees. Send them as JSON strings. Do not use binary floating point to align an order to an increment. Preserve IDs as strings, even when they contain only digits. Accept additional response fields for forward compatibility.
For GM assets, inspect the corresponding entry in
result.tokenConfig: id, decimals, ledgerUnit, custodiedAs, and sharesMultiplier. When ledgerUnit is shares, order sizes, fills, balances, and internal transfers use underlying share-equivalent units. custodiedAs names the external wrapper token.
For example, a SPY-USDC order with size: "2" trades two SPY-equivalent units at a price in USDC per unit. It does not request two SPYon wrapper tokens. If the applied multiplier is 1.02 shares per token, 10 wrapper tokens correspond to 10.2 share units before applicable precision handling. Internal wallet transfers do not apply the multiplier again. Deposit and withdrawal conversion occurs at the custody boundary; use the credited or executed amounts for reconciliation rather than recomputing past events using a later multiplier.
Refresh configuration on reconnect, before starting a trading session, and after a market-disabled or increment-related rejection. A listed asset, a tradable pair, a supported funding network, and eligibility as Perps collateral are distinct capabilities.
3. Fund the Spot wallet
The wallet identifiers arespot for Spot trading, margin for Perps collateral, and main for the main wallet. Funds in another wallet do not automatically fund a Spot order.
Read balances
GET /v1/spot/balances requires authentication and returns an array in result:
total is the held quantity. reserved is the quantity held by open Spot orders. free = total - reserved is available to fund additional orders, subject to account restrictions. usdValue is an optional estimate; it is not an executable price and can be zero when valuation is unavailable. Key rows by coin, not array position. Zero-balance rows can be returned.
For a limit buy, the funding requirement is price × size in the quote asset. For a sell, it is size in the base asset. Open-order reservations prevent those funds from being reused. A completed cancellation releases the reservation for the unfilled portion. Withdrawal availability is subject to its own checks and is not established by this endpoint.
Transfer funds
POST /v1/spot/transfers requires transfer. It accepts main ↔ spot and margin ↔ spot transfers on the same account. Margin transfers also require asset eligibility and sufficient withdrawable margin.
The following example moves 1,000 USDC from Perps collateral to Spot. Execute it only after selecting the amount to move:
id inside from and to defaults to the authenticated account ID. If supplied, both must identify that same account. Amount must be positive and conform to the asset’s decimals. The shared GET /v1/account endpoint returns the authenticated accountID when you need it.
The response result is a transfer record containing id, from, to, amount, symbol, time, and a numeric type. For a margin/spot move, the returned record represents the leg crediting your destination wallet; its from.wallet can therefore be main, even when the request starts from margin or spot. Preserve the response and read the destination balance to reconcile the move.
GET /v1/spot/transfers returns paginated Spot movement history. Supported filters are startTime, endTime, cursor, and limit. It includes Spot transfers, subaccount transfers involving Spot, and share adjustments; it excludes trading-fee transfers. Use fills to account for trading fees.
Transfer creation has no client-supplied idempotency key. After a timeout, inspect balances and transfer history before resubmitting. If the move cannot be reconciled, retain the account, asset, amount, time, and any returned transfer ID for support.
4. Read market data
Book request:
[price, size] of decimal strings. Level size is aggregate base quantity. Bids run from highest to lowest price; asks run from lowest to highest. An empty side can be represented by an empty array or null; treat either as no displayed liquidity. Snapshot quantities are absolute values.
Ticker rows include market, disabled, baseCurrency, quoteCurrency, lastPrice, bid, ask, baseVolume, quoteVolume, high, low, and priceChangePercent. Volume and high/low describe the trailing 24-hour window. A ticker or last trade does not guarantee a currently available execution price.
Public trades contain id, market, price, size, cost, aggressor_side, and time. aggressor_side is the taker’s side. These are market-wide trades; they do not identify your account’s fills.
For REST candles, from and to are Unix seconds. Supported resolutions are 1, 5, 15, 60, 240, 1D, and 1W (D and W are accepted aliases). countback requests a positive number of bars ending at to. Align time ranges to bar boundaries; the server can round the requested boundaries. to is inclusive. Each candle has startTime, open, high, low, close, and base-asset volume. Prices and volume are decimal strings; startTime is an RFC 3339 timestamp.
Minimum size and price protection
Order size must be a positive multiple ofbaseIncrement. The smallest permitted quantity is one baseIncrement; there is no separate minimum-notional requirement in the standard Spot order validator. For example, a market with baseIncrement: "0.01" accepts quantities such as 0.01, 0.02, and 1.23, but not 0.001 or 0.015.
A limit price must be positive, less than 10,000,000, and a multiple of quoteIncrement. Market orders use base-denominated size; omit price and timeInForce. Positive quoteSize is not supported. Meeting the increments does not guarantee acceptance: available funds, market access, liquidity, and price protection are checked separately.
Spot price protection uses the underlying oracle price. The configured percentage can vary; 10% is the fallback. GTC orders use directional price bounds, while market and IOC orders are checked using simulated execution. The rules below describe the boundaries and IOC partial-fill behavior.
Price protection rules
Protection percentage
The protection percentage can vary by market and market type. The default fallback is 10%; it is not a fixed promise for every market. A missing or zero reference price prevents an order from passing price validation.GTC limit orders
The check is directional: it prevents a buy priced too high or a sell priced too low. With reference priceR and configured protection fraction f, the ordinary bounds are:
- Buy:
limit price <= R × (1 + f). - Sell:
limit price >= R × (1 - f).
f away, though other validation rules still apply.
For example, if a Spot asset’s oracle price is 100 USDC and its configured fraction is 10%, a GTC buy limit above 110 or a sell limit below 90 is rejected by this check. Perpetual price-discovery bounds, when active, can tighten the corresponding boundary; those bounds do not apply to Spot.
Market and IOC limit orders
Market orders and IOC limit orders are checked by simulating execution against available liquidity. A market order is rejected if the simulation cannot fill the requested quantity within the system’s protective boundary. For an IOC limit order, execution is bounded by the tighter of your limit and the system boundary. When your limit is at least as restrictive as the system boundary, a partial fill is allowed and the remainder is canceled. A limit outside the system boundary is not automatically rejected solely for that limit; it must be fully executable within the system boundary during validation. Validation is not a fill guarantee. Review the returned order status and fills. Use an IOC limit order when you need to specify your own execution-price bound. See Order types.5. Place an order
POST /v1/spot/orders requires trade. Requests and responses use Content-Type: application/json.
quoteSize orders are unsupported and a positive quoteSize is rejected. Use base-denominated size for both buys and sells. Spot does not provide borrowing, a leverage field, or a reduce-only position workflow. Do not send builderCode or rpi, which are Perps-only. Do not send Perps stop-loss/take-profit fields or submit stopMarket or takeProfitMarket directly.
Limit order
This submits a buy for up to two SPY-equivalent units at 600 USDC per unit or better:postOnly: true to require that it rest; a post-only order that would immediately match is rejected. Post-only and IOC cannot be combined.
Immediate-or-cancel limit order
UsetimeInForce: "IOC" to execute available quantity at the limit price or better and cancel any remainder. A partial fill is possible; IOC is not fill-or-kill.
Market order
A market order consumes available liquidity. It may fill at multiple prices or partially fill. Use an IOC limit order when your instruction requires an explicit execution-price bound.price and timeInForce. A returned market-order price of "0" is an order-type marker, not the execution price. Read its fills or divide filledCost by filledSize when filledSize is positive. Market orders can be disabled independently; handle market_orders_disabled without assuming the order was placed.
Delayed placement
Some accounts or order flows may require the delayed placement paths:-
POST /v1/spot/orders/delayed -
POST /v1/spot/orders/batch/delayedThey accept the same bodies as their ordinary counterparts. If the server rejects placement with a message directing you to a delayed path, sign a new request using that exact path and a fresh timestamp. Do not reuse a signature from the original URL. Allow time for the server’s delay; an ambiguous timeout still requires reconciliation.
6. Track orders and fills
Order placement returnssuccess: true and an order object in result. It can already be partially filled, fully filled, or canceled when you receive the response. An acknowledgement does not guarantee that an order remains open.
Illustrative resting-order response:
An
open order can have nonzero filledSize; there is no separate partially-filled status. A canceled order can also have fills. In JSON responses the fully filled status is fullyfilled. The order-history query filter instead uses status=fullyFilled.
Get one order
client: is a path lookup prefix; do not include it in the submitted clientOrderId.
After a timeout, query the original clientOrderId before submitting another order. Reusing an accepted ID returns clientOrderID_collision; it does not return the original response. A not-found result immediately after a timeout is not proof that the original request cannot still complete. Keep the original ID and reconcile the order and fills before deciding to submit a replacement with a new ID.
List orders
GET /v1/spot/orders requires authentication. Supported parameters:
Read fills
These authenticated endpoints return your executions:market, startTime, endTime, cursor, and limit. Time filters are Unix milliseconds. The by-order endpoint returns an array without pagination.
Illustrative buy fill, as an item inside result:
id. isMaker identifies whether your order supplied resting liquidity for that execution. One order can have multiple fills and may incur both maker and taker fees across its lifetime.
7. Calculate fees and reconcile balances
Launch promotion: Spot maker and taker trading fees are 0% for the first 30 days after Spot launch. This window starts at launch, not at your first trade. The fee calculations below explain how nonzero rates are applied; use the rate returned for your account. See Fees. Read your maker/taker rates from authenticated market configuration. Rates are captured when an order is accepted; a later configuration change does not reprice a resting order’s captured rates. Fees are rounded per fill to the fee asset’s precision. Use returned fee amounts for accounting.
At an illustrative taker rate of
0.0002, a buy of 1 SPY unit at 600 USDC spends 600 USDC and receives 0.9998 SPY units. A sell of 1 SPY unit at the same price receives 599.88 USDC after a 0.12 USDC fee, subject to fee rounding.
The buy fill’s size remains "1"; it is not the net balance credit. Do not assume fee is always denominated in USDC, and do not subtract it twice when comparing against reported balances.
To reconcile an interval, account for fills and fees, wallet transfers, and share adjustments. Order events alone cannot explain every balance change.
8. Cancel orders and submit batches
All cancellation endpoints requiretrade.
The batch parameter is
orderIDs, with capital IDs, in the query string. It accepts exchange IDs or client:-prefixed client IDs. URL-encode the query and sign the encoded form. Cancel-all is scoped to Spot; it does not cancel Perps orders.
A cancel cannot undo a fill. After an ambiguous cancellation result, reread the order and fills. An already-filled or already-canceled response requires reconciliation rather than repeated blind cancellation.
Batch placement
POST /v1/spot/orders/batch accepts 1–20 orders:
result.addedOrders and result.failedOrders. Each failure contains order, error, and errorCode.
Batch cancellation similarly returns result.successfulCancels and result.failedCancels. Each cancellation failure contains orderId, error, and errorCode. Item-level errorCode uses camel case; the outer error envelope uses error_code.
A batch is not an atomic cancel-and-replace operation. Reconcile each order by ID, and resubmit only items whose outcomes are known.
9. Pagination and REST errors
Paginated endpoints put the result array and cursor metadata at the top level:limit must be an integer from 1 to 1,000; the request default is 1,000. The service may cap historical page size further. Start without a cursor, then pass pageInfo.nextCursor as the next request’s cursor, keeping filters unchanged. Stop when it is empty. Treat cursors as opaque strings and URL-encode them; do not decode or construct them. Do not infer completion solely from a page being shorter than your requested limit.
success. result can be omitted on errors or successful actions with no result, such as cancel-all. Preserve error_code for programmatic handling and treat error as descriptive text whose wording can change. Gateways or connection failures can return non-JSON errors.
Back off with jitter on HTTP 429 and transient read failures. Respect
Retry-After if present. For timeouts or server errors on writes, determine the original outcome before retrying.
10. Rate limits
REST rate limits are ceilings, not recommended polling rates. Clients should use WebSocket subscriptions for ongoing updates and keep REST reads limited to initialization, necessary lookups, and reconciliation. Limits can vary by account and environment; the standard defaults below are not a promise of throughput.
Additional account-level budgets apply separately to order additions and cancellations: standard defaults are 30 order actions/second with a burst of 60 for each action. These budgets are shared across markets, placement paths, and Spot and Perps. A batch of N orders consumes N actions. Additional API keys for the same account do not create new account budgets. Cancel-all has its own endpoint budget and is exempt from the shared cancellation-action budget.
Avoid continuous REST polling for order books, trades, orders, fills, or balances when the corresponding WebSocket channel is available. Apply incoming updates to local state; do not issue a REST refresh after every WebSocket message or fill. Use occasional, bounded REST reconciliation to detect drift, and reconcile after reconnects or ambiguous writes.
If a required channel is unavailable, use conservative fallback polling, coalesce reads across your application’s consumers, and back off on failures or HTTP 429. Stop fallback polling when the subscription is restored.
11. WebSocket authentication and subscriptions
Connect to/ws on the same environment as REST. Public market-data subscriptions can be used without login. Private channels require a successful login.
For API-key login, sign:
ondo_perps_ws_login is used for Spot as well as Perps. There is no path or body in the signed message. The timestamp uses milliseconds and the same acceptance window as REST.
Send json.dumps(websocket_login()) from the Python helper over your WebSocket connection. Its JSON shape is:
type: "loggedIn" before subscribing to private channels. A connection logs into one account. Reconnecting requires a new login and new subscriptions.
Channels
Subscribe using
op: "subscribe". For book, order, fill, and trade subscriptions, specify the markets you need. Omitting markets subscribes to the channel’s default market set; an explicit list makes the scope predictable. Unsubscribe with op: "unsubscribe" and the same channel and subscription parameters.
limit caps the number of returned levels per side. Optional depthLevels is a price grouping increment, expressed as a decimal string, not a count of levels. Omit it or use "0" for ungrouped prices. An unsubscribe for a custom depth subscription must match its depthLevels and limit.
For kLineSpot, specify exactly one market and a resolution from 1, 5, 15, 1H, 4H, 1D, or 1W. WebSocket hour resolutions differ from REST’s 60 and 240:
data is an object with m (market), t (event time), s (bar start), e (bar end), o/h/l/c (open/high/low/close), v (base volume), and x (whether the bar is closed). t, s, and e are Unix seconds. Unlike REST candles, WebSocket candle prices and volume are JSON numbers. Decode these numbers with a decimal-aware JSON parser, such as Python’s json.loads(message, parse_float=Decimal). Update the current bar by market and start time until it closes.
Message handling
Responses usetype, optional channel, timestamp, and optional data, code, or msg. Handle loggedIn, subscribed, unsubscribed, update, pong, and error.
Illustrative depth update:
ordersSpot and fillsSpot provide events, not a complete historical snapshot at subscription. Initialize open orders and historical fills through REST. balanceSpot provides complete balance snapshots and can coalesce multiple changes; it is not a transaction log. Replace your balance map when a new snapshot arrives. If the balance channel is unavailable, use REST balances alongside order and fill events.
Send an application-level heartbeat regularly, for example every 15 seconds, and monitor for a response:
type: "pong". Reconnect with backoff when the connection closes or heartbeats stop. A quiet market alone is not evidence of a broken connection.
Recover after a disconnect
- Reconnect, log in, and resubscribe. Discard book state from the old connection.
- Buffer incoming private events while retrieving open orders, fills from an overlapping interval, balances, and transfers through REST.
- Deduplicate fills by
id. Merge order updates byorderId, preserving cumulative fills and terminal outcomes; requery an order if REST and buffered events disagree. - Refresh balances after reconciliation. These endpoints and channels do not share an atomic account snapshot or a global sequence number.
- Resume order submission after the client has reconciled outstanding orders and funds.
12. Cancel on inactivity
Arm the Spot dead man’s switch on the connection responsible for your trading session:markets. Inbound application messages, including ping, refresh the inactivity timer; outbound market updates do not. Keep heartbeats well inside the configured interval.
The timer remains armed if the connection closes. It is account-wide, so coordinate multiple clients: arming it on another connection replaces the account’s previous Spot timer. It does not distinguish orders created by separate API keys or sessions. Do not rely on it as durable protection across service restarts; reconnect, reconcile, and re-arm it.
To disarm, send either:
13. Market pauses and share adjustments
Affected markets can pause for corporate actions or other trading restrictions. Stop submitting new orders when the API reports that trading is unavailable. Preserve outstanding IDs and reconcile their final states before creating replacements. Share adjustments can change a GM asset’s balance without a fill. Read share-adjustment entries in Spot transfer history and refresh balances and market metadata after the event. Do not rescale historical fills using the current shares multiplier. Do not assume an old resting order was preserved, canceled, or resized solely because trading resumed; query it.14. Additional REST exports
Authenticated CSV exports are available atGET /v1/spot/orders/csv and GET /v1/spot/fills/csv. These responses are CSV files, not the JSON envelope consumed by the Python helper. Order CSV export includes only orders with fills; it is not a source for the complete open-order set.
The trading integration covered here uses /v1/spot/*. Use the explicit Spot paths for order management, history, and market data.