# Synthetix Mainnet > Developer documentation for Synthetix perpetual contracts ## Changelog ### This page tracks significant changes to the Synthetix API documentation and structure. ### April 28, 2026 Changes relevant to 3rd-party integrators for the Synthetix release on 2026-04-28. #### Summary | Action | Change Type | Breaking? | Action Required | | ----------------------------------------------------- | --------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 30-day historical data cap on time-ranged queries | Behavior change | Possibly | Clamp `startTime` to at most 30 days in the past for `getFundingPayments`, `getPositionHistory`, `getTrades`, `getTransfers` (`POST /v1/trade`) and `getFundingRateHistory` (`POST /v1/info`). Handle HTTP 400 when the window falls outside the allowed range. | | Orderbook WebSocket — empty sides now `[]` not `null` | Behavior change | Possibly | Replace `if (data.bids === null)` / `if (data.asks === null)` null checks with empty-array checks (`length === 0` or equivalent). | | `getWithdrawableAmounts` | New endpoint | No | Optionally query per-asset withdrawable amounts by passing a `symbols` array (max 100) to `POST /v1/trade getWithdrawableAmounts`. Returns `totalWithdrawableUsdt` and an `items` array with per-asset `symbol`, `withdrawableAmount`, `quantity`, `pendingWithdraw`, and `withdrawFee`. | REST changes are on `POST /v1/trade` and `POST /v1/info` where noted. The rate-limit change applies to the trade WebSocket (`/v1/ws/trade`) as well. The orderbook null → empty-array change is on the info WebSocket (`/v1/ws/info`). ### April 21, 2026 Changes relevant to 3rd-party integrators for the Synthetix release on 2026-04-21. #### Summary | Action | Change Type | Breaking? | Action Required | | ----------------------------------------------------------------------- | --------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Unrecognized trade `direction` now errors | Behavior change | Possibly | If you receive a new `500` where you previously got a successful response, this indicates corrupted trade direction data on the backend requiring operator investigation. | | Linked TP/SL sibling cancellation | Behavior change | Possibly | Remove any reconciliation logic that expects the auto-cancelled sibling of a filled `normalTpsl` pair to appear in `getOrderHistory`. | | `getOpenInterest` `timestamp` field | Behavior change | No | Update any code that assumed `timestamp` was always `0` — real Unix ms values are now returned. | | `getExchangeStatus` | New capability | No | Optionally call `GET /v1/exchange/status` (REST service) or `GET /v1/ws/exchange/status` (WebSocket service, plain HTTP — no upgrade) to poll exchange status; this endpoint stays available while other public actions return HTTP 503 during maintenance. | | WebSocket `marginUpdate` notifications | New Capability | No | subscribe to `subAccountUpdates` on `/v1/ws/trade` to receive real-time `subAccountEvent` notifications with `eventType: "marginUpdate"` reflecting margin state changes. | | WebSocket trade events — `position` and trade metadata | Response Change | No | Allow additive `position` (nullable), `direction`, `orderId`, `tradeId`, `order.venueId`, `order.clientId`, `isTaker`, `maker`, `reduceOnly`, and `postOnly` fields on `subAccountEvent` with `eventType: "trade"`. | | Conditional order trigger fields on `getOrderHistory` / `getOpenOrders` | Response Change | No | Allow additive `triggerPrice` (string) and `triggerPriceType` (`"mark_price"` or `"last_price"`) on response items. Populated for conditional orders (stop market, stop limit, take profit limit, take profit market); omitted for non-conditional orders. | | WebSocket funding payment events | Response Change | No | The `subAccountUpdates` subscription now emits `SubAccount_Funding` notifications with `eventType: "funding"`, including `paymentId`, `positionSize`, `fundingRate`, `payment`, `markPrice`, and four timestamp fields. | REST changes are available on `POST /v1/trade` and `POST /v1/info` where noted. WebSocket changes are on `/v1/ws/trade` (subaccount-scoped events) and `/v1/ws/info` (public market data) where applicable. *** ### April 15, 2026 Changes relevant to 3rd-party integrators for the Synthetix release on 2026-04-15. #### Summary | Action | Change Type | Breaking? | Action Required | | --------------------------------------- | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `getPerformanceHistory` PnL calculation | Behavior change | Possibly | Reconcile downstream PnL charts, reports, alerts, and tests against the new equity-delta calculation. | | `getBalanceUpdates` `amount` field | Behavior change | Yes | `amount` on withdrawals is now the post-fee net value; migrate to `grossAmount` if you relied on `amount` being the gross withdrawal value. | | `getRateLimits` live data | Behavior change | Possibly | `getRateLimits` now returns real per-subaccount rate-limit state; remove any assumption it returns static placeholder values. | | `getTradesForPosition` on WebSocket | New capability | No | `getTradesForPosition` is now callable on `/v1/ws/trade` in addition to REST. | Changes span REST (`POST /v1/trade`) and the trade WebSocket (`/v1/ws/trade`). #### Breaking change details ##### `getPerformanceHistory` — PnL now excludes external cash flows `getPerformanceHistory` now derives `pnl` from account equity movement with net external cash flows removed, instead of combining realized trade PnL, funding payments, and the latest unrealized PnL snapshot in each bucket. If you compare `getPerformanceHistory.pnl` against internal analytics, dashboards, alerts, or snapshot-based tests, expect historical values to change. Deposits, withdrawals, and transfers should no longer appear as trading performance. ##### `getBalanceUpdates` — `amount` is now net for withdrawals For withdrawals, `amount` is now the net value after the $5 withdrawal fee. Two new fields cover the difference: * `fee` (string): withdrawal fee charged. * `grossAmount` (string): original gross withdrawal amount (`amount + fee`). Integrators using `amount` to track gross withdrawal values should migrate to `grossAmount`. *** ### April 7, 2026 Changes relevant to 3rd-party integrators for the Synthetix release on 2026-04-07. #### Summary | Action | Change Type | Breaking? | Action Required | | -------------------------------------------------------------------------- | --------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `orderbookUpdate` depth and diff payloads | Behavior change | Yes | Stop reading `data.timestamp` from orderbook event payloads (field removed); parse envelope `met` as a JSON string (decimal Unix µs) rather than a JSON integer. | | `tradeUpdate` envelope `met` field | Behavior change | Yes | Parse `met` as a JSON string (decimal Unix µs) rather than a JSON integer. | | `placeOrders` new `limitGtd` order type | New capability | No | Optionally use `orderType: "limitGtd"` with required `expiresAt` (Unix seconds, between 10 s and 24 h ahead) to submit Good Till Date limit orders. | | `getOpenOrders`, `getOrderHistory`, `placeOrders` resting/filled responses | Response change | No | Allow additive optional `expiresAt` (Unix ms) on open order, order history, and `placeOrders` resting/filled status items; allow additive `cancelReason` string on order history items. | | `subAccountUpdates` order events | Response change | No | Allow additive `expiresAt` (Unix ms) and `cancelReason` on all order event payloads (placed, modified, cancelled, etc.). | | `subAccountUpdates` trade executed events | Response change | No | Allow additive `direction`, `markPrice`, `entryPrice`, `maker`, `reduceOnly`, `postOnly`, and `triggeredByLiquidation` on trade event payloads. | | `subAccountUpdates` delegation events | New capability | No | Expect new `delegationAdded` and `delegationRevoked` event types on the `subAccountUpdates` channel. | Trade and order changes are available on REST (`POST /v1/trade`) and the trade WebSocket (`/v1/ws/trade`). Delegation events and orderbook changes are WebSocket-only (`/v1/ws/trade` and `/v1/ws/info` where applicable). #### Breaking change details ##### `orderbookUpdate` — `data.timestamp` removed and `met` now a string **`data.timestamp` removal:** The `timestamp` field was previously present on both depth snapshot and diff payloads (`data.timestamp`, a string). It is no longer emitted. Clients reading `data.timestamp` for event timing should migrate to the envelope-level `met` field. **`met` type change:** The `met` field in the orderbook notification envelope was previously a JSON integer (Unix microseconds). It is now a JSON string containing the same decimal value (e.g., `"met": "1712345678901234"`). Update any JSON parsing that expects a numeric type. ##### `tradeUpdate` — `met` now a string The `met` field in trade WebSocket notification envelopes was previously a JSON integer (Unix microseconds). It is now a JSON string containing the same decimal value (e.g., `"met": "1712345678901234"`). Update any JSON parsing that expects a numeric type. *** ### 31 March 2026 Changes relevant to 3rd-party integrators since the Synthetix release on 2026-03-24. #### Summary | Action | Change Type | Breaking? | Action Required | | ------------------------------------------------------ | --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `placeOrders` quantity validation | Behavior change | Possibly | If you match exact validation messages, update handling for `quantity is missing`, `quantity is negative`, `quantity is invalid`, and `quantity is zero`. `closePosition: true` orders still allow empty or zero quantity. | | WebSocket subscribe acknowledgements | Behavior change | Yes | Do not expect a `seq` field in subscribe acknowledgements, and do not expect an immediate salutation frame before the first real `candles` or `subAccountUpdates` message. | | `cancelOrders` | Behavior change | No | No wire-format change; `CANCEL_NOT_FOUND` now keeps in-memory order state when a fill is still in flight, avoiding dropped maker fills and balance inconsistencies. | | `createSubaccount` / `getSubAccount` | Response change | No | Allow additive `collaterals[].price`, `collaterals[].calculatedAt`, and `marginSummary.debt` fields. | | `subAccountUpdates` margin summary | Response change | Possibly | Allow additive `debt` in margin summary event payloads, and do not assume `withdrawable` is derived only from the updated collateral entries in the event. | | `collateralPrices` | New capability | No | Optional: subscribe on `/ws/trade` or `/ws/info` for real-time collateral price updates, with optional per-symbol filtering. | | `getPositionHistory` | Response change | No | Allow additive optional `leverage` on position history items. | | `getTrades` | Response change | No | Allow additive `orderType` on trade history items. | | `getPositions` / `subAccountUpdates` position payloads | Response change | No | Allow additive `adlBucket` in REST positions and WebSocket position payloads. | Changes are available on the API surfaces implied by each row. Trade actions are exposed on REST (`POST /v1/trade`) and the trade WebSocket (`/v1/ws/trade`); info actions on REST (`POST /v1/info`) and the info WebSocket (`/v1/ws/info`) where applicable. `collateralPrices` is WebSocket-only. *** ### 24 March 2026 Changes relevant to 3rd-party integrators for the release of Synthetix on 2026-03-24. #### Summary | Action | Change Type | Breaking? | Action Required | | --------------------------------------------------------- | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `scheduleCancel` | New endpoint | No | Allow clients to schedule automatic cancellation of all orders if they fail to reset the timer within a specified timeout period. | | Max-order rejection codes | Behavior change | Possibly | If you match exact order-limit error codes, replace `maxOrdersExceeded` handling with `maxOrdersPerMarket` and `maxTotalOrders`. | | `getSubAccountIds` | Behavior change | Possibly | Send `walletAddress` in EIP-55 checksum form. | | `clientOrderId` / canonical symbol validation | Behavior change | Possibly | Whitespace before/after that was previously trimmed will now cause an error. | | `getFundingPayments` | Behavior change | Possibly | Set an explicit `limit` if you relied on unbounded queries; values above `1000` are now rejected (default `100`). | | `placeOrders` / `modifyOrder` / `cancelOrders` error rows | Response change | Possibly | Order IDs (`venueId`, `clientId`) are now included in error entries. `venueId` may be `null` when no venue order ID has been assigned yet. | | `getOpenOrders` / `getOrderHistory` | Response change | No | Optional `clientOrderId` filter was added. | | `createSubaccount` / `getSubAccount` | Response change | No | New `accountLimits.maxOrdersPerMarket`, `accountLimits.maxTotalOrders`, `collateralValue`, and `adjustedCollateralValue` fields were added. | | `getDelegationsForDelegate` | Response change | No | New `adjustedAccountValue` field was added. | All changes are available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`) unless otherwise noted. *** ### 17 March 2026 Changes relevant to 3rd-party integrators for the release of Synthetix on 2026-03-17. #### Summary | Action | Change Type | Breaking? | Action Required | | ---------------------------- | --------------- | --------- | ----------------------------------------------------------------------- | | WebSocket `id` / `requestId` | Behavior change | Possibly | Ensure `id` and `requestId` are JSON strings, not numbers | | `clientOrderId` | Behavior change | Yes | Remove leading/trailing whitespace from client order IDs | | `triggerPriceType` | Behavior change | Possibly | Use exactly `"mark"` or `"last"` (case-sensitive, no trim) | | Request body size | Behavior change | Possibly | Keep request bodies ≤ 20 KB or handle HTTP 413 | | `createSubaccount` name | Behavior change | Possibly | Reject names with leading/trailing whitespace | | Time params conflict | Behavior change | Possibly | Do not send both `startTime` and `fromTime` (or `endTime` and `toTime`) | | Rate limits | Behavior change | Possibly | Expect updated limits; see deployment config | | `cancelOrders` by cloid | New capability | No | Optional: use `clientOrderIds` instead of `venueOrderIds` | | `modifyOrder` by cloid | New capability | No | Optional: use `clientOrderId` instead of `venueOrderId` | | `getPositionHistory` | New endpoint | No | Query closed position history | | `getCollaterals` (info) | New endpoint | No | Fetch collateral configs from info API | | `getDelegationsForDelegate` | Response change | No | New `accountValue` field per delegation | | Order rejections | Response change | No | New `errorCode` field for programmatic handling | | Collateral tiers | Response change | No | New `valueAddition`, `valueRatio` in tier objects | | Candle timeframes | New capability | No | 30m, 8h, 12h, 3d, 3m supported | All changes are available on both REST (`POST /v1/trade`, `POST /v1/info`) and WebSocket (`/v1/ws/trade`, `/v1/ws/info`) unless otherwise noted. #### WebSocket `id` / `requestId` — Behavior change WebSocket requests must send `id` and `requestId` as JSON **strings**. Numeric values are rejected. ##### What you need to do * Ensure `id` and `requestId` in outgoing WebSocket messages are strings, e.g. `"id": "req-123"` not `"id": 123`. * Handle HTTP 400 with message `"The id must be a string"` or `"The requestId must be a string"` if your client sends numbers. ##### Error message change * **Before:** `"Invalid JSON"` * **After:** `"The id must be a string"` or `"The requestId must be a string"` when the parse failure is due to numeric `id`/`requestId`. #### `clientOrderId` — Behavior change Leading and trailing whitespace in `clientOrderId` is now **rejected** instead of trimmed. ##### What you need to do * Strip whitespace from `clientOrderId` before sending. Values like `" my-order-1 "` will return HTTP 400. * Validate that your client order IDs contain no leading or trailing spaces. #### `triggerPriceType` — Behavior change `triggerPriceType` now accepts only exact matches: `"mark"` or `"last"`. Case-insensitive matching and trimming are no longer applied. ##### What you need to do * Send exactly `"mark"` or `"last"`. Values like `"MARK"`, `" Mark "`, or `"LAST"` will be rejected. * Empty string still defaults to `"mark"`. #### Request body size limit — Behavior change Request bodies larger than 20 KB are rejected with HTTP 413. ##### What you need to do * Keep request bodies under 20 KB. For large batches, split into multiple requests. * Handle HTTP 413 and `"Request body too large"` in error responses. #### `createSubaccount` name validation — Behavior change Subaccount names with leading or trailing whitespace are rejected. ##### What you need to do * Trim subaccount names before sending. Values like `" My Account "` will return HTTP 400 with a message about leading or trailing whitespace. #### Time query params conflict — Behavior change Endpoints that support time ranges now enforce consistency. Do not send both `startTime` and `fromTime`, or both `endTime` and `toTime`, with different values. ##### What you need to do * Prefer `startTime` and `endTime`. `fromTime` and `toTime` remain supported but are deprecated. * Do not send conflicting pairs (e.g. `startTime` and `fromTime` with different values). This returns HTTP 400. #### Non-breaking changes * `**cancelOrders`\*\* — Can cancel by `clientOrderIds` instead of `venueOrderIds`. Same action, new optional param shape. * `**modifyOrder**` — Can modify by `clientOrderId` instead of `venueOrderId`. Same action, new optional param shape. * `**getPositionHistory**` — New endpoint. Query closed position history with `symbol`, `startTime`, `endTime`, `limit`, `offset`. * `**getCollaterals**` — New info endpoint. Returns collateral configs with tiers. Replaces subaccount-specific collateral fetch. * `**getDelegationsForDelegate**` — New `accountValue` field (string, USDT) per delegated account. * **Order rejections** — New `errorCode` field for place/cancel/modify responses. See [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) for codes and retry policy. * **Collateral tiers** — New `valueAddition` and `valueRatio` fields in tier objects. * **Candle timeframes** — Added 30m, 8h, 12h, 3d, 3m to `getCandles`. * **Rate limits** — Updated values; consult deployment configuration for current limits. *** ### 16 March 2026 #### `getSubAccountIds` — delegation support added The `getSubAccountIds` endpoint now accepts an optional `includeDelegations` boolean parameter. * **Default behavior unchanged**: omitting `includeDelegations` (or setting it to `false`) returns a flat array of owned subaccount IDs, identical to the previous response. * **New**: when `includeDelegations: true`, the response is an object with two fields: * `subAccountIds` — subaccount IDs owned by the wallet address * `delegatedSubAccountIds` — subaccount IDs for which the wallet address has been granted delegate access This applies to both the REST (`/v1/info`) and WebSocket (`/v1/ws/info`) interfaces. **No change to request or response format for callers that do not send `includeDelegations`.** *** ### 16 March 2026 #### `getMarketPrices` — field documentation corrected The `fundingRate` and `timestamp` fields in the `getMarketPrices` response are now correctly documented. Previous documentation incorrectly stated these fields return zero / empty values. Both fields are populated: * **`fundingRate`**: Returns the current estimated funding rate for each market, sourced from the funding rate service. * **`timestamp`**: Returns a Unix millisecond timestamp representing when the response was generated. **No change to request format or response structure.** This is a documentation correction only. *** ### 14 March 2026 #### `updateSubAccountName` — new endpoint documented Added documentation for the `updateSubAccountName` endpoint, which allows renaming an existing trading subaccount. Available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`). * **Request**: Requires `subAccountId` and `name` parameters with EIP-712 signature * **Response**: Returns the updated `subAccountId` and `name` * **Authentication**: Subaccount-level EIP-712 signing with `UpdateSubAccountName` typed data *** ### 18 March 2026 #### `getSubAccount` — collateral value fields now populated The `collaterals` array in the `getSubAccount` response now includes `adjustedCollateralValue` and `collateralValue` for each collateral entry. These fields were always present in the response schema but previously returned empty strings. They now return the USD-equivalent collateral values: * `collateralValue`: full collateral value before haircut * `adjustedCollateralValue`: collateral value after applying the haircut discount This applies to both the REST and WebSocket variants of `getSubAccount`, as well as `getSubAccounts`. **No change to request format or other response fields.** *** ### 13 March 2026 #### `getTransfers` — endpoint documentation added Published REST and WebSocket documentation for the `getTransfers` endpoint. This endpoint retrieves transfer history for a subaccount, including collateral transfers between subaccounts. **Key details:** * Supports filtering by collateral symbol and time range (max 30 days) * Pagination via `limit` (default 50, max 1000) and `offset` * Uses `SubAccountAction` EIP-712 signing (no nonce required) * Available on both REST (`POST /trade`) and WebSocket (`method: "post"`) protocols *** ### 10 March 2026 #### `cancelAllOrders` — response docs corrected The `message` field in `cancelAllOrders` response items is populated only when a cancellation fails for a specific order. On success, `message` is an empty string. Previous documentation incorrectly showed `"Order cancelled successfully"` as the success message value. The `symbol` field (the market symbol for the cancelled order) is now documented in both the REST and WebSocket response examples, consistent with the actual response contract. **No change to request format, signing, or other response fields.** *** ### 11 March 2026 Changes relevant to 3rd-party integrators for the release of Synthetix on 2026-03-11. #### Summary | Action | Change Type | Breaking? | Action Required | | ------------------------------------------------------- | --------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `addDelegatedSigner` | Behavior change | Yes | Send exactly one permission value and update delegation-management flows for real `delegate` vs `session` semantics. | | `removeDelegatedSigner` | Behavior change | Yes | Expect hierarchical removal rules and owner-side cascade deletion of child session signers. | | All WebSocket channels excl. `orderbookUpdate` | Behavior change | Possibly | New `seq` field added to WebSocket channels: update parsers to accept new fields; implement gap detection using seq. | | `requestId` / `X-Request-ID` / WebSocket `id` | Behavior change | Possibly | Send an explicit valid client request ID if you rely on echoed correlation IDs; stop assuming the server always generates one in the payload. | | `createSubaccount` | Behavior change | Possibly | Treat tier-limit and validation failures as structured `400` errors, not generic retriable `500`s. | | `cancelOrders` | Response change | Possibly | Remove any client workaround that interpreted `error` items as successful cancellations. | | `cancelAllOrders` | Response change | Possibly | Update response parsing of the `message` field, which is now error-only; success returns empty/null. | | `orderbookUpdate` | Behavior change | Yes | Orderbook now supports opt-in diff mode: subscribe per symbol, not `ALL`, and either request `format: "snapshot"` or implement diff merge with `meseq`/`prevMeseq`/`checksum` validation. | | `candleUpdate` | Response change | Possibly | Handle an initial salutation payload before candle OHLC updates. | | `subAccountUpdate` | Response change | Possibly | Tolerate `seq`/provenance fields and resync from authoritative state when `needsResync=true`. | | `getSubAccountIds` | Response change | No | No change unless you opt into `includeDelegations=true`; then parse an object instead of a flat array. | | `getDelegatedSigners` / `getDelegationsForDelegate` | Response change | No | Allow additive delegation fields such as `addedBy`, `cascadeRemovedSigners`, and optional `owningAddress`. | | `createSubaccount` / `getSubAccount` / `getSubAccounts` | Response change | No | Allow the additive `accountLimits.maxSubAccounts` field. | | `placeOrders` | Response change | No | If you decode status rows strictly, allow `order` on rejected status items. | | `marketPriceUpdate` / `trade` | Response change | No | Allow additive sequencing, funding-rate, and provenance fields on websocket notifications. | All changes are available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`) unless otherwise noted. #### `addDelegatedSigner` — behavior change Delegation permissions are now enforced as real permission tiers instead of loosely treated aliases. What you need to do: * Send exactly one permission in `permissions`. * Use `delegate` for manager-style signers and `session` for trading/session keys. * Do not rely on delegates or session signers being able to create arbitrary downstream delegations. ##### Permission changes | Field | Before | After | Notes | | -------------------- | ----------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `permissions` | Any non-empty array was broadly tolerated in practice | Exactly one value is required | Multi-value arrays are rejected. | | `permissions[0]` | Semantically loose | `delegate`, `session`, or legacy `trading` only | `trading` remains accepted for compatibility but is normalized to `session`. | | Delegation authority | Loose / flattened behavior | Hierarchical | Owner can create `delegate` or `session`; delegate can create/manage only session signers it created; session signer cannot create delegations. | #### `removeDelegatedSigner` — behavior change Delegation removal now follows the same hierarchy as delegation creation, and owner removals can cascade. What you need to do: * Re-test any flow where a delegate or session signer removes other signers. * If you remove a delegate as the owner, handle the optional `cascadeRemovedSigners` list. ##### Removal changes | Field | Before | After | Notes | | ----------------- | ------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | | Removal authority | Looser behavior | Hierarchical | Delegate can remove only session signers it created; session signer cannot remove delegations. | | Response | `subAccountId`, `walletAddress` | Adds optional `cascadeRemovedSigners` | Returned when owner removal also deletes child session signers created by the removed delegate. | #### `requestId` / `X-Request-ID` / WebSocket `id` — behavior change Client correlation IDs are now explicitly client-owned instead of always being echoed from a server-generated internal request ID. What you need to do: * On REST, send a valid `X-Request-ID` header if you rely on `requestId` / `request_id` in the response body. * On WebSocket, send a valid bounded `id` (or `requestId`) if you rely on echoed request IDs in replies. * Stop assuming responses always contain a request ID when you did not send one. ##### Correlation ID changes | Surface | Before | After | Notes | | --------------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------- | ------------------------------------------------------------ | | REST top-level `requestId` / `request_id` | Server-generated internal ID was echoed | Client-provided `X-Request-ID` is echoed | Omitted when the header is absent. | | `transferCollateral.response.requestId` / `withdrawCollateral.response.requestId` | Reflected internal request ID | Reflects client request ID | Empty when the client does not provide one. | | WebSocket response `id` / `requestId` | Server side request field was echoed | Only client-provided ID is echoed | Omitted when the client omits it. | | Validation | Minimal | Client IDs are validated | Invalid IDs are rejected instead of being silently accepted. | #### `createSubaccount` — behavior change Some creation failures now return structured client/business errors instead of generic internal errors. What you need to do: * Handle `400` responses for tier-limit and validation failures as normal non-retriable API outcomes. * If you key business logic off error codes, add handling for `MAX_SUB_ACCOUNTS_EXCEEDED` and inspect `error.details`. ##### Error behavior * **Before:** Tier/precondition failures could surface as generic `500` "Failed to create subaccount". * **After:** Tier/precondition failures can return `400` with structured error data such as `code: "MAX_SUB_ACCOUNTS_EXCEEDED"` and metadata like `currentCount`, `maxAllowed`, and `tierName`. #### `cancelOrders` — response change The per-item status mapping now matches the actual cancellation outcome. What you need to do: * Expect successful cancellations under `statuses[].canceled`. * Expect failed cancellations under `statuses[].error`. * Remove any workaround that treated `error` entries as successful cancels. ##### Status mapping | Field | Before | After | Notes | | --------------------- | ----------------------------------------------- | ----------------------------------- | ------------------------ | | `statuses[].canceled` | Could be missing on successful cancellations | Present on successful cancellations | Correct success mapping. | | `statuses[].error` | Could be populated for successful cancellations | Only populated on actual failures | Correct failure mapping. | #### `orderbookUpdate` — behavior change Orderbook subscriptions are now negotiated and default to incremental diff delivery. What you need to do: * Stop subscribing with `symbol: "ALL"` for `orderbook`. * If you want old-style full snapshots, subscribe with `format: "snapshot"`. * If you use the default `format: "diff"`, implement merge logic plus `meseq` / `prevMeseq` continuity checks and `checksum` validation. ##### Subscription contract | Field | Before | After | Notes | | -------------------------- | ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | | `params.symbol` | Specific symbol or `ALL` | Specific symbol only | `ALL` is now rejected for `orderbook`. | | `params.format` | Not supported | `diff` or `snapshot` | Default is `diff`. | | `params.depth` | Not supported | `10`, `50`, `100` | Default is `50`. | | `params.updateFrequencyMs` | Not supported | `50`, `100`, `250`, `500`, `1000` | Default is `250`; `depth=100` only supports `250/500/1000`. | | Notification payload | Repeated full-book snapshots | Initial snapshot, then diffs by default | Diff messages include only changed levels; deletions use `quantity: "0"`. | | Envelope | Basic notification | Adds `meseq`, `prevMeseq`, `met`, `checksum`, `type` | Required to validate diff continuity and integrity. | #### `candleUpdate` — response change New candle subscriptions now receive an immediate salutation frame before normal candle data. What you need to do: * Branch on `data.eventType`. * Ignore or handle the initial salutation payload before parsing candle OHLC fields. ##### Initial frame * **Before:** First message after subscribe was a normal candle payload. * **After:** The server may first send: | Field | Before | After | Notes | | ---------------- | -------------------------- | -------------------------- | ---------------------------------------- | | `data.eventType` | Not present | `"salutation"` | Sent immediately after subscribe. | | `data.message` | Not present | `"hello"` | Greeting payload. | | `data.timestamp` | Candle data timestamp only | Greeting publish timestamp | Separate from candle OHLC payload shape. | #### `subAccountUpdate` — response change Account-stream notifications now carry sequencing and recovery metadata. What you need to do: * Accept additive envelope fields such as `seq`, `meseq`, `met`, and `provenanceMissing`. * If you receive `needsResync=true`, refresh authoritative account state before trusting further incremental updates. * If you parse rejected order events, allow `data.reason`. ##### Envelope fields | Field | Before | After | Notes | | -------------------------------- | ----------- | ---------------- | --------------------------------------------------------- | | `seq` | Not present | Present | Monotonic per subaccount stream. | | `meseq`, `met` | Not present | Optional | Added on matching-derived order/trade events. | | `provenanceMissing` | Not present | Optional boolean | True when matching-derived provenance is incomplete. | | `needsResync` | Not present | Optional boolean | Signals the client to reconcile from authoritative state. | | `data.reason` on rejected orders | Not present | Present | Additive rejection detail. | #### Non-breaking changes * `**getSubAccountIds`\*\* — New opt-in `includeDelegations=true` request param on `POST /v1/info`. Without it, the response stays a flat owned-ID array. With it, the response becomes `{ subAccountIds, delegatedSubAccountIds }`. * `**getDelegatedSigners`\*\* — Response adds nullable `addedBy`. * `**getDelegationsForDelegate**` — Accepts optional `owningAddress` so a session signer can query a parent delegate's delegations after authorization checks. * `**removeDelegatedSigner**` — Response may include additive `cascadeRemovedSigners`. * `**createSubaccount**`, `**getSubAccount**`, and `**getSubAccounts**` — Responses add `accountLimits.maxSubAccounts`. * `**placeOrders**` — Rejected status rows can include `order` alongside `error`, which improves per-item correlation. * `**marketPriceUpdate**` — Subscribe-time cached snapshots are marked with `isSnapshot: true`; notifications now include per-stream `seq`, and payloads may include `fundingRate` and `nextFundingTime`. * `**trade**` — Public trade notifications add `meseq`, `met`, and `provenanceMissing`. * `**subscribe` acknowledgement\*\* — Successful websocket subscribe responses can include current `seq`; `orderbook` subscribe responses also echo negotiated `depth`, `format`, and `updateFrequencyMs`. *** ### March 3, 2026 Changes relevant to 3rd-party integrators for the release of Synthetix on 2026-03-03. *** #### Summary | Action | Change Type | Breaking? | Action Required | | ------------------- | --------------- | --------- | --------------------------------------------------- | | `All Actions` | Behavior change | Possibly | Ensure request rates stay within IP limits. | | `All Trade Actions` | Behavior change | Possibly | Ensure request rates stay within subaccount limits. | | `getOrderHistory` | Response change | No | None. | All changes are available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`) unless otherwise noted. *** #### `All Actions` — Behavior change Global rate limiting has been expanded across all API endpoints. ##### What you need to do * Ensure your integration handles HTTP 429 (Too Many Requests) responses gracefully. * Review your request volume to ensure it stays within the new per-IP and per-subaccount token budgets. ##### Rate Limiting Changes * **Per-IP Limits:** A new per-IP rate limit now applies to *all* actions (both trade and info endpoints). * **Expanded Subaccount Limits:** The per-subaccount rate limit, which previously only applied to `placeOrders`, now applies to *all* trade actions. * **Token Costs:** Different actions now consume different amounts of rate limit tokens. For `placeOrders`, the token cost scales by the number of orders in the batch. * **Authentication-First (REST):** For REST requests, rate limiting now occurs *after* authentication, using the verified `subAccountId` rather than the unauthenticated value from the payload. * **Global Enforcement:** Rate limits are now enforced globally across all API instances using a Redis-backed token bucket, rather than being tracked in-memory per instance. *** #### Non-breaking changes * **`getOrderHistory`** — Added a new `updateTime` field (timestamp) to the response items, indicating when the order was last updated. *** ### February 23, 2026 Changes relevant to 3rd party integrators between releases `release-all-20260212` (Feb 12) and `release-all-20260223` (Feb 23). *** ### Summary | Action | Change Type | Breaking? | Action Required | | -------------------------------- | --------------- | --------- | --------------------------------------------------------------------------------------------------------- | | `placeOrders` | Response change | No | Migrate from `resting.id`/`filled.id`/`canceled.id` to `resting.order`/`filled.order`/`canceled.order` | | `placeOrders` | Behavior change | Possibly | `clientOrderId` format relaxed — update validation if you enforce the old hex format | | `cancelOrders` | Response change | No | Migrate from `canceled.id` to `canceled.order` | | `cancelAllOrders` | Response change | No | Migrate from `orderId` (string) to `order` (composite) | | `cancelAllOrders` | Behavior change | Possibly | `symbols` now required; use `["*"]` to cancel all markets | | `modifyOrder` | Response change | No | Migrate from `orderId` to `order`; response is now a typed object | | `getOpenOrders` | Response change | No | Migrate `orderId` → `order`, `takeProfitOrderId` → `takeProfitOrder`, `stopLossOrderId` → `stopLossOrder` | | `getOrderHistory` | Response change | No | Migrate from `orderId` to `order` | | `getTrades` | Response change | Possibly | `orderId` → `order`; `clientOrderId` **removed** (now in `order.clientId`) | | `getPositions` | Response change | No | Migrate `takeProfitOrderIds` → `takeProfitOrders`, `stopLossOrderIds` → `stopLossOrders` | | `getBalanceUpdates` | Response change | Possibly | `id` changed from `number` to `string`; new `TRANSFER` filter; new `fromSubAccountId` field | | `getTransfers` | New endpoint | No | Implement if needed | | `transferCollateral` | Behavior change | Possibly | Failed transfers now return structured errors instead of gRPC errors | | WebSocket `order` events | Response change | No | Migrate from `orderId` to `order` | | WebSocket `tradeExecuted` events | Response change | No | Migrate from `orderId` to `order` | | WebSocket `trade` notifications | Response change | No | New `channel` and `timestamp` fields added | All changes are available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`) unless otherwise noted. *** ### Composite Order ID — cross-cutting change Most order-related endpoints and WebSocket events now return a composite `order` field instead of (or alongside) the plain `orderId` string. This is the single largest change in this release and affects many actions listed in the summary. #### New `order` field shape ```json { "order": { "venueId": "123456789", "clientId": "my-order-abc" } } ``` | Field | Type | Description | | ---------- | -------- | ---------------------------------------------------------------------- | | `venueId` | `string` | System-generated order identifier (same value previously in `orderId`) | | `clientId` | `string` | Client-provided order ID. Empty string `""` if none was set. | #### Deprecation of `orderId` The old `orderId` field is still present in all responses and contains the venue-only ID (as a string). It is **deprecated** and will be removed in a future release. Migrate to the `order` field. #### What you need to do Update your response parsing to read from the `order` object. You can do this gradually — the deprecated fields continue to work during the transition. The table below lists every deprecated → new field mapping across all affected endpoints. #### Deprecated → new field mapping by endpoint | Endpoint / Event | Old field(s) | New field(s) | Notes | | ------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------- | | `placeOrders` | `resting.id`, `filled.id`, `canceled.id` | `resting.order`, `filled.order`, `canceled.order` | Each is `{ venueId, clientId }` | | `cancelOrders` | `canceled.id` | `canceled.order` | Same shape as `placeOrders` statuses | | `cancelAllOrders` | `orderId` | `order` | | | `modifyOrder` | `orderId` | `order` | Response is now a typed object with explicit fields | | `getOpenOrders` | `orderId`, `takeProfitOrderId`, `stopLossOrderId` | `order`, `takeProfitOrder`, `stopLossOrder` | TP/SL fields omitted when absent | | `getOrderHistory` | `orderId` | `order` | | | `getTrades` | `orderId`, `clientOrderId` | `order` | `clientOrderId` is **removed**, not deprecated — use `order.clientId` | | `getPositions` | `takeProfitOrderIds` (string\[]), `stopLossOrderIds` (string\[]) | `takeProfitOrders` (object\[]), `stopLossOrders` (object\[]) | Arrays of `{ venueId, clientId }` | | WS `order` events | `orderId` | `order` | All event types (accepted, filled, cancelled, modified, etc.) | | WS `tradeExecuted` | `orderId` | `order` | | | WS `trade` notifications | — | `channel`, `timestamp` | Additive only — new fields `"trade"` and server timestamp in ms | All deprecated fields remain present during the transition period unless marked **removed** above. *** ### `placeOrders` — response change The `placeOrders` response has a nested structure that differs from other endpoints, so a full example is shown here. Each status in the `statuses` array (`resting`, `filled`, `canceled`) now contains a composite `order` field alongside the deprecated `id`. #### Response example ```json { "status": "ok", "response": { "statuses": [ { "resting": { "order": { "venueId": "123456789", "clientId": "my-order-1" }, "id": "123456789" } }, { "filled": { "order": { "venueId": "987654321", "clientId": "" }, "id": "987654321", "totalSize": "0.5", "avgPrice": "95000.00" } } ] } } ``` `resting.id`, `filled.id`, and `canceled.id` are deprecated (still present, contain venue ID only). `filled.totalSize` and `filled.avgPrice` are unchanged. *** ### `placeOrders` — `clientOrderId` format relaxation #### What you need to do If you validate `clientOrderId` on your side before sending, update your validation. If you were not using `clientOrderId`, no action needed. #### What changed * **Before:** `clientOrderId` must be a 32-character hex string with `0x` prefix (66 chars total, matching `/^0x[a-fA-F0-9]{32}$/`) * **After:** `clientOrderId` can be any string up to 255 characters containing alphanumeric characters and `.-/+_=` #### New validation rules | Rule | Value | | --------------------------- | ------------------------------------------------- | | Max length | 255 characters | | Allowed characters | `a-z`, `A-Z`, `0-9`, `.`, `-`, `/`, `+`, `_`, `=` | | Leading/trailing whitespace | Trimmed automatically | | Required | No (still optional) | *** ### `cancelAllOrders` — behavior change #### What you need to do If you currently send `cancelAllOrders` without `symbols` or with an empty `symbols` array, you must now provide at least one symbol. To cancel across **all markets**, send `["*"]`. #### What changed * `symbols` field is now **required and must be non-empty**. Previously, an empty or missing `symbols` array was accepted. * A new `["*"]` wildcard cancels orders across all markets. * The wildcard must be the sole element — `["*", "ETH-USDT"]` is invalid. #### Request examples Cancel all orders on specific markets (unchanged): ```json { "params": { "action": "cancelAllOrders", "symbols": ["BTC-USDT", "ETH-USDT"] } } ``` Cancel all orders across all markets (new): ```json { "params": { "action": "cancelAllOrders", "symbols": ["*"] } } ``` #### New error cases | Condition | Error | | ---------------------------- | ------------------------------------------------------------------ | | `symbols` is `[]` or missing | `"symbols must be nonempty"` | | `["*", "ETH-USDT"]` | `"symbols[0]: wildcard \"*\" must be the only element in symbols"` | *** ### `getTrades` — response change Migrate from `orderId` to `order`. Note that the standalone `clientOrderId` field has been **removed** (not just deprecated) — it is now accessible only as `order.clientId`. #### Response example (single trade) ```json { "tradeId": "555", "order": { "venueId": "100", "clientId": "my-order-1" }, "orderId": "100", "symbol": "BTC-USDT", "side": "buy", "price": "95000.00", "quantity": "0.1", "realizedPnl": "0", "fee": "4.75", "feeRate": "0.0005", "timestamp": 1740307200000, "maker": false, "reduceOnly": false, "markPrice": "95010.00", "entryPrice": "95000.00", "triggeredByLiquidation": false, "direction": "long", "postOnly": false } ``` *** ### `getBalanceUpdates` — response change #### What you need to do 1. If you parse the `id` field as a number, switch to parsing it as a string. 2. If you use `actionFilter`, note the new `TRANSFER` filter and the changed error message format for invalid filters. 3. Optionally handle the new `fromSubAccountId` field for transfer records. #### Field changes | Field | Before | After | | ------------------ | ----------------------- | ------------------------------------------ | | `id` | `number` | `string` | | `action` values | `DEPOSIT`, `WITHDRAWAL` | `DEPOSIT`, `WITHDRAWAL`, `TRANSFER` | | `fromSubAccountId` | not present | `string` (optional, present for transfers) | #### `actionFilter` changes * New valid filter value: `TRANSFER` * **Error message format change:** * **Before:** `"actionFilter must be 'DEPOSIT', 'WITHDRAWAL', or comma-separated combination"` * **After:** `"invalid action filter, available filters: DEPOSIT, WITHDRAWAL, TRANSFER"` #### Response example (transfer record) ```json { "id": "42", "subAccountId": "2011391943438766080", "action": "TRANSFER", "status": "success", "amount": "100", "collateral": "USDT", "timestamp": 1740307200000, "fromSubAccountId": "2011391943438766080", "toSubAccountId": "3022502054549877191" } ``` *** ### `getTransfers` — new endpoint Returns transfer history for a subaccount, including collateral transfers between subaccounts. Supports filtering by symbol and time range, with pagination. #### What you need to do Sign using the existing `SubAccountAction` EIP-712 type (same as `getSubAccount`) with `action` set to `"getTransfers"`. No `nonce` required (read-only action). #### Request ```json { "params": { "action": "getTransfers", "symbol": "USDT", "limit": 50, "offset": 0, "startTime": 1740220800000, "endTime": 1740307200000 } } ``` | Field | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------------------- | | `params.action` | `string` | Yes | Must be `"getTransfers"` | | `params.symbol` | `string` | No | Filter by collateral symbol (e.g. `"USDT"`) | | `params.limit` | `number` | No | Max results. Default `50`, max `1000` | | `params.offset` | `number` | No | Pagination offset. Default `0` | | `params.startTime` | `number` | No | Start timestamp in ms | | `params.endTime` | `number` | No | End timestamp in ms. Max range: 30 days | #### Response ```json { "status": "ok", "response": { "transfers": [ { "transferId": "12345", "from": "2011391943438766080", "to": "3022502054549877191", "symbol": "USDT", "amount": "100", "transferType": "COLLATERAL_TRANSFER", "status": "success", "requestId": "req_abc123", "timestamp": 1740307200000 } ], "total": 1 } } ``` | Field | Type | Description | | -------------- | -------- | -------------------------------------------- | | `transferId` | `string` | Transfer identifier | | `from` | `string` | Source subaccount ID | | `to` | `string` | Destination subaccount ID | | `symbol` | `string` | Collateral symbol | | `amount` | `string` | Transfer amount | | `transferType` | `string` | Transfer type (e.g. `"COLLATERAL_TRANSFER"`) | | `status` | `string` | Transfer status | | `requestId` | `string` | Original request ID | | `errorMessage` | `string` | Error message (omitted if empty) | | `timestamp` | `number` | Transfer timestamp in ms | | `total` | `number` | Total number of matching transfers | #### Errors | HTTP Status | Condition | Error Code | | ----------- | --------------------------------------------- | ------------------ | | `400` | Invalid format or missing `subaccountId` | `VALIDATION_ERROR` | | `400` | Invalid time range (exceeds 30 days) | `VALIDATION_ERROR` | | `400` | Negative `limit` or `offset`, or limit > 1000 | `VALIDATION_ERROR` | | `500` | Internal service failure | `INTERNAL_ERROR` | *** ### `transferCollateral` — behavior change #### What you need to do If you rely on error responses from failed transfers, update your error handling. Failed transfers now return structured error responses with specific error codes instead of opaque gRPC errors. Request and response schemas for successful transfers are unchanged. #### What changed Previously, transfer failures (e.g. insufficient balance) were returned as gRPC-level errors, often resulting in generic `500` responses. Now, failed transfers return structured JSON error responses with appropriate HTTP status codes: | Error condition | HTTP Status | Error Code | | ------------------- | ----------- | --------------------- | | Asset not found | `400` | `ASSET_NOT_FOUND` | | Insufficient margin | `400` | `INSUFFICIENT_MARGIN` | | Invalid value | `400` | `INVALID_VALUE` | | Validation error | `400` | `VALIDATION_ERROR` | | Internal failure | `500` | `INTERNAL_ERROR` | *** ### February 6, 2026 #### API Changelog: release-all-20260205 & release-all-20260206 Changes relevant to 3rd party integrators across releases `release-all-20260205` (Feb 5) and `release-all-20260206` (Feb 6). ##### Summary | Action | Change Type | Breaking? | Action Required | | --------------------------- | --------------- | --------- | ------------------------------------------------------- | | `getSubAccounts` | New endpoint | No | Implement if needed | | `removeAllDelegatedSigners` | New endpoint | No | Implement if needed; requires new EIP-712 type | | `withdrawCollateral` | Behavior change | Possibly | Update error message parsing if you match on fee errors | | `getTrades`, `placeOrders` | Internal only | No | None. Wire format unchanged. | All changes are available on both REST (`POST /v1/trade`) and WebSocket (`/v1/ws/trade`). ##### `getSubAccounts` — new endpoint Returns all subaccounts under the authenticated user's master account. Works for both owners and delegates. Each subaccount includes its delegated signers and fee rate tier. ##### What you need to do Sign using the existing `SubAccountAction` EIP-712 type (same as `getSubAccount`) with `action` set to `"getSubAccounts"`. No `nonce` required (read-only action). ##### Request Identical to `getSubAccount`. The only difference is `params.action`: ```json { "params": { "action": "getSubAccounts" } } ``` No additional parameters. ##### Response Returns `{ subAccounts: [...] }` where each element has the same shape as an existing `getSubAccount` response, plus two additional fields: `feeRates` and `delegatedSigners`. **New fields on each subaccount object (compared to `getSubAccount`):** | Field | Type | Description | | ------------------ | ------------------- | ---------------------------------------------------------------------- | | `feeRates` | `FeeRateInfo` | Fee tier for this subaccount | | `delegatedSigners` | `DelegatedSigner[]` | All delegates authorized on this subaccount. Empty `[]` if none exist. | **`FeeRateInfo` fields:** | Field | Type | Example | Description | | -------------- | -------- | ---------------- | ------------------------ | | `makerFeeRate` | `string` | `"0.0002"` | Maker fee rate (decimal) | | `takerFeeRate` | `string` | `"0.0005"` | Taker fee rate (decimal) | | `tierName` | `string` | `"Regular User"` | Fee tier name | **`DelegatedSigner` fields:** | Field | Type | Example | Description | | --------------- | ---------------- | --------------------------- | ---------------------------------------------- | | `subAccountId` | `string` | `"2011391943438766080"` | Subaccount this delegation applies to | | `walletAddress` | `string` | `"0xDelegateWalletAddress"` | Delegate's Ethereum address | | `permissions` | `string[]` | `["trading"]` | Granted permissions | | `expiresAt` | `number \| null` | `1738800000000` | Expiry as unix ms, or `null` for no expiration | ##### Errors | HTTP Status | Condition | Error Code | | ----------- | ------------------------ | ------------------ | | `400` | Missing `subaccountId` | `VALIDATION_ERROR` | | `500` | Internal service failure | `INTERNAL_ERROR` | ##### `removeAllDelegatedSigners` — new endpoint Atomically removes all delegated signers from a subaccount. Owner-only: delegates receive a `403` error. ##### What you need to do Add the new `RemoveAllDelegatedSigners` EIP-712 primary type to your signing code. This is a write action, so `nonce` is required. **New EIP-712 type to add:** ``` RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] ``` Use the standard EIP-712 domain (same as all other actions). Sign with `primaryType: "RemoveAllDelegatedSigners"`. ##### Request `params` contains only `action` — no additional action-specific fields: ```json { "params": { "action": "removeAllDelegatedSigners" }, "subaccountId": "123", "nonce": 1738700000000, "signature": { "v": 27, "r": "0x...", "s": "0x..." }, "expiresAfter": 1738700060000 } ``` | Field | Type | Required | Description | | --------------- | -------- | -------- | ------------------------------------------ | | `params.action` | `string` | Yes | Must be `"removeAllDelegatedSigners"` | | `subaccountId` | `string` | Yes | Subaccount to remove all delegates from | | `nonce` | `number` | Yes | Timestamp in ms (replay protection) | | `signature` | `object` | Yes | EIP-712 signature `{ v, r, s }` | | `expiresAfter` | `number` | No | Request expiry as unix ms. `0` = no expiry | ##### Response ```json { "status": "ok", "response": { "subAccountId": "123", "removedSigners": ["0xDelegateAddr1", "0xDelegateAddr2"] } } ``` | Field | Type | Description | | ---------------- | ---------- | --------------------------------------------------------- | | `subAccountId` | `string` | The subaccount from which signers were removed | | `removedSigners` | `string[]` | Wallet addresses of removed delegates. Empty `[]` if none | ##### Errors | HTTP Status | Condition | Error Code | | ----------- | ------------------------------------ | ------------------ | | `400` | Invalid format or validation failure | `VALIDATION_ERROR` | | `403` | Caller is a delegate, not the owner | `VALIDATION_ERROR` | | `404` | Subaccount not found | `NOT_FOUND` | | `500` | Internal service failure | `INTERNAL_ERROR` | ##### `withdrawCollateral` — behavior change ##### What you need to do If you parse or pattern-match on withdrawal fee error messages, update your matching. Request and response schemas are unchanged. ##### What changed Withdrawal fees are now per-asset in native units instead of a flat $5 USD. The minimum withdrawal amount must exceed the asset-specific fee. | Asset | Fee | Approximate USD equivalent | | ------ | --------- | -------------------------- | | `USDT` | `5` | $5 | | `WETH` | `0.0017` | \~$5 at \~$3,000/ETH | | `BTC` | `0.00005` | \~$5 at \~$100,000/BTC | ##### Error message format change * **Before:** `"withdrawal amount must be greater than $5 fee"` * **After:** `"withdrawal amount must be greater than {fee} {asset} fee"` Examples of the new format: * `"withdrawal amount must be greater than 5 USDT fee"` * `"withdrawal amount must be greater than 0.0017 WETH fee"` * `"withdrawal amount must be greater than 0.00005 BTC fee"` ##### Net amount ### The on-chain withdrawal amount remains `amount - fee`. If you display estimated net amounts to users, note that the fee now varies by asset. Withdrawal fees are not yet exposed via a public API endpoint. ### January 30, 2026 #### WebSocket Notifications: New `channel` Field Replaces `method` WebSocket notification messages now use a `channel` field instead of `method` to identify the notification type. This change provides naming consistency with the rest of the API (which uses camelCase). ##### What Changed All WebSocket notification messages now include: * **`channel`** (new): Identifies the notification type using camelCase naming * **`timestamp`** (new): Unix timestamp in milliseconds when the message was sent ##### Channel Names | Old `method` Value | New `channel` Value | | ------------------------ | ------------------- | | `candle_update` | `candleUpdate` | | `market_price_update` | `marketPriceUpdate` | | `orderbook_depth_update` | `orderbookUpdate` | | `subAccountEvent` | `subAccountUpdate` | ##### New Message Format ```json { "channel": "candleUpdate", "data": { ... }, "timestamp": 1706659200000 } ``` ##### Migration Update your WebSocket message handlers to check the `channel` field: **Before:** ```javascript if (message.method === "candle_update") { handleCandleUpdate(message.data); } ``` **After:** ```javascript if (message.channel === "candleUpdate") { handleCandleUpdate(message.data); } ``` ##### Affected Subscriptions * [Candle Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/candleUpdates) * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) * [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) * [SubAccount Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) ##### Notes * Subscription requests still use `"method": "subscribe"` - only notification messages have changed * The `method` field has been removed from notification messages * The new `timestamp` field provides the server-side timestamp for when the notification was sent *** ### January 6, 2026 #### Breaking Change: Nonce Removed from SubAccountAction Requests SubAccountAction endpoints no longer require the `nonce` parameter in requests or EIP-712 signatures. ##### Affected Endpoints * `getPositions` * `getOpenOrders` * `getOrderHistory` * `getTrades` * `getFundingPayments` * `getSubAccount` * `getDelegatedSigners` * `getBalanceUpdates` ##### What Changed * **EIP-712 Type**: `SubAccountAction` no longer includes `nonce` field * **Request Body**: `nonce` parameter removed from request structure * **Signature**: Sign only `subAccountId`, `action`, and optional `expiresAfter` ##### New SubAccountAction EIP-712 Type ```javascript SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] ``` ##### Migration **Before:** ```json { "params": { "action": "getPositions", "subAccountId": "123456789" }, "nonce": 1735689600000, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` **After:** ```json { "params": { "action": "getPositions", "subAccountId": "123456789" }, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` ##### Notes * The `expiresAfter` parameter remains optional for request expiration * Trading endpoints (`placeOrders`, `cancelOrders`, `modifyOrder`) still require `nonce` * Update your EIP-712 signing code to remove `nonce` from the message for SubAccountAction endpoints *** ### December 31, 2025 #### Fixed: EIP-712 Signature Documentation for placeOrders Corrected EIP-712 type definitions in all placeOrders documentation to match backend implementation. ##### What Changed * **Getting Started Guide**: Fixed Python and JavaScript examples to include complete EIP-712 types * **WebSocket placeOrders**: Corrected type name from `NewOrdersRequest` to `PlaceOrders`, fixed `subAccountId` type from `uint64` to `uint256`, added missing `closePosition` and `postOnly` fields to all usage examples * **Code Examples**: Applied ES6 object shorthand syntax for cleaner, modern JavaScript ##### EIP-712 Types Fixed The `PlaceOrders` type now correctly includes all required fields: ```javascript PlaceOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orders", type: "Order[]" }, { name: "grouping", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "orderType", type: "string" }, { name: "price", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "quantity", type: "string" }, { name: "reduceOnly", type: "bool" }, { name: "isTriggerMarket", type: "bool" }, { name: "clientOrderId", type: "string" }, { name: "closePosition", type: "bool" } ] ``` Previously missing fields: `subAccountId`, `grouping`, `clientOrderId`, `closePosition` ##### Impact This fixes signature validation failures that were preventing market maker integrations from placing orders successfully. *** ### December 14, 2025 #### Breaking Change: getSubAccounts → getSubAccount The `getSubAccounts` endpoint has been replaced with `getSubAccount` for retrieving subaccount information. ##### What Changed * **Endpoint**: `getSubAccounts` → `getSubAccount` * **Request**: Now requires `params.subAccountId` parameter * **Response**: Returns single subaccount object instead of array * **EIP-712 Signature**: Uses `SubAccountAction` type with fields: `subAccountId`, `action`, `expiresAfter` (nonce removed as of January 6, 2026) ##### Migration ```javascript // Before { "params": { "action": "getSubAccounts" } } // Returns: array of all subaccounts // After { "params": { "action": "getSubAccount", "subAccountId": "123456789" } } // Returns: single subaccount object ``` *** ### December 1, 2025 * Begin tracking changes * Only implemented methods shown *** ## Deposits & Account Creation This guide covers how to deposit collateral into your Synthetix trading account and how accounts are created. ### Overview Deposits are on-chain transactions made directly to the `SynthetixDepositContract` smart contract. Your first deposit automatically creates your trading account. :::info For withdrawals, see the API documentation: * [REST API: Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/rest-api/trade/withdrawCollateral) * [WebSocket: Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/withdrawCollateral) ::: ### Contract Addresses | Network | Contract | Address | | ------- | -------------------------------- | -------------------------------------------- | | Mainnet | SynthetixDepositContract (Proxy) | `0xD62595c3c23B690BAEE0935e107A209Cb1Dbd37B` | | Mainnet | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | | Mainnet | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | Sepolia | SynthetixDepositContract (Proxy) | `0x5Ed4a299E9fa36E6bDb4E0723bD3ad9D233f33A0` | | Sepolia | USDT (Mock) | `0xE02d1640dcaB494327DbE4B63fBcD441b92c8205` | ### Supported Collateral Currently supported collateral types: | Asset | Decimals | Network | | ----- | -------- | ------- | | USDT | 6 | Mainnet | Each collateral has configurable limits: * **Minimum Deposit**: Minimum amount per deposit transaction * **Maximum Balance**: Maximum total balance per user * **Minimum Withdrawal**: Minimum amount per withdrawal :::info Contact the team for current limit values, as these may be adjusted based on market conditions. ::: ### Deposit Flow #### Account Creation Your first deposit automatically creates a **master account**. No separate account creation step is required. **Understanding Account IDs:** * **`subAccountId`**: The unique identifier for any account (master or sub). This is a non-zero value (e.g., `987654321`) generated when the account is created. * **`masterId`**: Indicates the account hierarchy: * `masterId = 0` means this account IS the master account * `masterId = ` means this is a subaccount belonging to that master When depositing, passing `subAccountId: 0` in the request is a special value that means "deposit to my master account" - it does not mean the master account has ID 0. #### Standard Deposit (ERC20 Approve + Deposit) Deposits require two transactions: 1. **Approve**: Allow the deposit contract to spend your tokens 2. **Deposit**: Call the deposit function on the contract ##### JavaScript Example (ethers.js v6) ```javascript import { ethers } from 'ethers'; // Contract addresses (Mainnet) const DEPOSIT_CONTRACT = '0xD62595c3c23B690BAEE0935e107A209Cb1Dbd37B'; const USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; // ABI for deposit function const depositABI = [ 'function deposit((address token, uint256 amount, address beneficiary, uint256 subAccountId, (((address token, uint256 amount) permitted, uint256 nonce, uint256 deadline) permit, bytes signature) permitDetails)[] deposits)' ]; const erc20ABI = [ 'function approve(address spender, uint256 amount) returns (bool)', 'function allowance(address owner, address spender) view returns (uint256)' ]; async function deposit(signer, amount) { const walletAddress = await signer.getAddress(); // Step 1: Approve USDT spending (if needed) const usdt = new ethers.Contract(USDT, erc20ABI, signer); const currentAllowance = await usdt.allowance(walletAddress, DEPOSIT_CONTRACT); if (currentAllowance < amount) { console.log('Approving USDT...'); const approveTx = await usdt.approve(DEPOSIT_CONTRACT, amount); await approveTx.wait(); console.log('Approval confirmed'); } // Step 2: Deposit const depositContract = new ethers.Contract(DEPOSIT_CONTRACT, depositABI, signer); const tx = await depositContract.deposit([{ token: USDT, amount: amount, beneficiary: walletAddress, subAccountId: 0, // 0 = deposit to master account (auto-created on first deposit) permitDetails: { permit: { permitted: { token: ethers.ZeroAddress, amount: 0 }, nonce: 0, deadline: 0 }, signature: '0x' } }]); console.log('Deposit transaction sent:', tx.hash); const receipt = await tx.wait(); console.log('Deposit confirmed in block:', receipt.blockNumber); return receipt; } // Usage const provider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'); const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const amount = ethers.parseUnits('1000', 6); // 1000 USDT await deposit(signer, amount); ``` ##### Python Example (web3.py) ```python from web3 import Web3 import os # Contract addresses (Mainnet) DEPOSIT_CONTRACT = '0xD62595c3c23B690BAEE0935e107A209Cb1Dbd37B' USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7' # Connect to Ethereum w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY')) account = w3.eth.account.from_key(os.environ['PRIVATE_KEY']) # ERC20 ABI (minimal) erc20_abi = [ { "name": "approve", "type": "function", "inputs": [ {"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"} ], "outputs": [{"type": "bool"}] } ] # Deposit contract ABI (minimal) deposit_abi = [ { "name": "deposit", "type": "function", "inputs": [{ "name": "deposits", "type": "tuple[]", "components": [ {"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "beneficiary", "type": "address"}, {"name": "subAccountId", "type": "uint256"}, {"name": "permitDetails", "type": "tuple", "components": [ {"name": "permit", "type": "tuple", "components": [ {"name": "permitted", "type": "tuple", "components": [ {"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"} ]}, {"name": "nonce", "type": "uint256"}, {"name": "deadline", "type": "uint256"} ]}, {"name": "signature", "type": "bytes"} ]} ] }] } ] def deposit(amount_usdt): amount = amount_usdt * 10**6 # USDT has 6 decimals # Step 1: Approve usdt = w3.eth.contract(address=USDT, abi=erc20_abi) approve_tx = usdt.functions.approve(DEPOSIT_CONTRACT, amount).build_transaction({ 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'gas': 100000, 'gasPrice': w3.eth.gas_price }) signed_approve = account.sign_transaction(approve_tx) tx_hash = w3.eth.send_raw_transaction(signed_approve.raw_transaction) w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Approval confirmed: {tx_hash.hex()}") # Step 2: Deposit deposit_contract = w3.eth.contract(address=DEPOSIT_CONTRACT, abi=deposit_abi) deposit_tx = deposit_contract.functions.deposit([{ 'token': USDT, 'amount': amount, 'beneficiary': account.address, 'subAccountId': 0, # 0 = deposit to master account (auto-created on first deposit) 'permitDetails': { 'permit': { 'permitted': {'token': '0x0000000000000000000000000000000000000000', 'amount': 0}, 'nonce': 0, 'deadline': 0 }, 'signature': b'' } }]).build_transaction({ 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'gas': 200000, 'gasPrice': w3.eth.gas_price }) signed_deposit = account.sign_transaction(deposit_tx) tx_hash = w3.eth.send_raw_transaction(signed_deposit.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Deposit confirmed in block: {receipt['blockNumber']}") return receipt # Usage deposit(1000) # Deposit 1000 USDT ``` #### Depositing to a Specific Subaccount Once you have created additional subaccounts under your master account, you can deposit directly to them by specifying their `subAccountId`: ```javascript // Deposit to a specific subaccount const tx = await depositContract.deposit([{ token: USDT, amount: amount, beneficiary: walletAddress, subAccountId: 987654321, // Specific subaccount ID permitDetails: { permit: { permitted: { token: ethers.ZeroAddress, amount: 0 }, nonce: 0, deadline: 0 }, signature: '0x' } }]); ``` :::note If the specified `subAccountId` doesn't exist or doesn't belong to the beneficiary wallet, the deposit will automatically be routed to the master account instead. ::: #### Gasless Deposits (Permit2) For a better user experience, you can use [Permit2](https://github.com/Uniswap/permit2) to combine approval and deposit into a single signature + transaction: 1. Sign a Permit2 message authorizing the transfer 2. Call `deposit()` with the permit details This avoids the separate approval transaction. The Permit2 contract address is `0x000000000022D473030F116dDEE9F6B43aC78BA3` (same on all networks). :::tip Permit2 deposits are especially useful for integrations where you want to minimize user friction. ::: ### Verifying Your Deposit After submitting a deposit transaction, you can verify it was successful in two ways: #### 1. Check Transaction Receipt The deposit emits an `AssetDeposited` event: ```javascript // Check for AssetDeposited event in receipt const AssetDepositedTopic = ethers.id( "AssetDeposited(address,address,address,uint256,uint256)" ); const depositEvent = receipt.logs.find( log => log.topics[0] === AssetDepositedTopic ); if (depositEvent) { console.log('Deposit successful!'); // Parse event data: depositor, beneficiary, token, amount, subAccountId } ``` #### 2. Query via API After a short indexing delay (typically 5-30 seconds), your deposit will appear in the API: ```javascript import axios from 'axios'; async function verifyDeposit(subAccountId, signature, expiresAfter) { const response = await axios.post('https://papi.synthetix.io/v1/trade', { params: { action: 'getBalanceUpdates', subAccountId: subAccountId, actionFilter: 'DEPOSIT', limit: 5 }, signature: signature, expiresAfter: expiresAfter }); return response.data.response.balanceUpdates; } ``` See [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) for full API documentation. #### Verification Flow 1. **Submit transaction**: Call `approve()` then `deposit()` on the contract 2. **Get receipt**: Transaction receipt contains `AssetDeposited` event 3. **Wait for indexing**: Events are indexed within \~5-30 seconds 4. **Query API**: Call `getBalanceUpdates` to confirm deposit appears in history ### Troubleshooting | Issue | Cause | Solution | | -------------------------- | -------------------------- | ------------------------------------------------------------- | | Transaction reverts | Insufficient approval | Ensure you've approved enough tokens for the deposit contract | | "Amount below minimum" | Deposit too small | Check minimum deposit requirements | | "Exceeds maximum" | User or global cap reached | Contact team about limits | | Deposit not showing in API | Indexing delay | Wait 30-60 seconds and retry | ### Related * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - View deposit and withdrawal history * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Check current balances and margin status * [Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/rest-api/trade/withdrawCollateral) - Withdraw funds via API * [Environments](https://developers.synthetix.io/environments) - API endpoints and configuration ## Environments The Synthetix API currently provides a mainnet environment for production trading. ### Mainnet **Live trading with real assets on Ethereum Mainnet.** #### Endpoints | Type | URL | | ------------------- | ----------------------------------------------------- | | **REST API** | `https://papi.synthetix.io/v1/trade` (authenticated) | | **REST Info** | `https://papi.synthetix.io/v1/info` (public) | | **WebSocket Trade** | `wss://papi.synthetix.io/v1/ws/trade` (authenticated) | | **WebSocket Info** | `wss://papi.synthetix.io/v1/ws/info` (public) | #### Blockchain | Property | Value | | ------------ | ---------------- | | **Network** | Ethereum Mainnet | | **Chain ID** | `1` | #### EIP-712 Domain ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; ``` #### Required Headers | Header | Value | Required For | | ------------ | ------------------------------------------------------------ | ------------------------------- | | `User-Agent` | A descriptive user agent string identifying your application | All WebSocket and REST requests | Example user agent strings: * `my-trading-bot/1.0.0` * `my-company-name/arbitrage-service` * `my-app-name (contact@example.com)` ### Configuration Example ```javascript const config = { // REST tradeUrl: 'https://papi.synthetix.io/v1/trade', infoUrl: 'https://papi.synthetix.io/v1/info', // WebSocket wsTradeUrl: 'wss://papi.synthetix.io/v1/ws/trade', wsInfoUrl: 'wss://papi.synthetix.io/v1/ws/info', // Required headers headers: { 'User-Agent': 'my-trading-bot/1.0.0' }, // EIP-712 domain: { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" } }; ``` ### Testnet Testnet environment is currently in development. ### Next Steps * [Getting Started](https://developers.synthetix.io/getting-started) - Quick start with code examples * [Authentication](https://developers.synthetix.io/developer-resources/api/authentication) - EIP-712 signing * [REST API](https://developers.synthetix.io/developer-resources/api/rest-api) - REST endpoint reference * [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) - Real-time data streaming ## Getting Started Quick start guide for integrating with the Synthetix Trading API. ### Prerequisites * **Ethereum wallet** with private key * **Node.js 18+** or **Python 3.8+** * **Basic understanding** of EIP-712 signatures * **Funded account** via the app (currently for select users only) For API endpoints and connection details, see [Environments](https://developers.synthetix.io/environments). ### Order Types The Synthetix API uses a flat structure for order types: | Order Type | Description | Required Fields | Example | | ----------- | ------------------------- | --------------- | ------------------------------------------------------------------------ | | `market` | Market order | - | `{orderType: "market"}` | | `limitGtc` | Limit Good Till Canceled | `price` | `{orderType: "limitGtc", price: "50000"}` | | `limitIoc` | Limit Immediate or Cancel | `price` | `{orderType: "limitIoc", price: "50000"}` | | `triggerSl` | Trigger Stop Loss | `triggerPrice` | `{orderType: "triggerSl", triggerPrice: "45000", isTriggerMarket: true}` | | `triggerTp` | Trigger Take Profit | `triggerPrice` | `{orderType: "triggerTp", triggerPrice: "55000", isTriggerMarket: true}` | #### Order Structure ```javascript { symbol: "BTC-USDT", side: "buy" | "sell", quantity: "0.01", orderType: "limitGtc", // Flat enum value price: "50000", // Required for limit orders, empty string for market triggerPrice: "", // Required for trigger orders, empty string otherwise isTriggerMarket: false, // true for market execution on trigger reduceOnly: false // Position reduction only } ``` #### Field Requirements by Order Type | Order Type | `price` | `triggerPrice` | `isTriggerMarket` | Notes | | ----------- | ------------- | -------------- | ----------------- | --------------------------------------------- | | `market` | `""` (empty) | `""` (empty) | `false` | Executes immediately at market price | | `limitGtc` | Required | `""` (empty) | `false` | Limit order, good till canceled | | `limitIoc` | Required | `""` (empty) | `false` | Immediate or cancel | | `triggerSl` | `""` or price | Required | `true`/`false` | Stop loss: `true` = market, `false` = limit | | `triggerTp` | `""` or price | Required | `true`/`false` | Take profit: `true` = market, `false` = limit | ### Quick Start Example #### JavaScript/TypeScript ```bash npm install ethers axios ``` ```javascript const { ethers } = require('ethers'); const axios = require('axios'); const PRIVATE_KEY = process.env.PRIVATE_KEY; class SynthetixClient { constructor(privateKey) { this.wallet = new ethers.Wallet(privateKey); // EIP-712 domain this.domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; // API base URL this.apiBaseUrl = 'https://papi.synthetix.io'; // Required headers this.headers = { 'User-Agent': 'my-trading-bot/1.0.0' }; } getNonce() { return Date.now(); } // Get subaccount IDs for a wallet (public endpoint, no auth) async getSubAccountIds(walletAddress) { const response = await axios.post(`${this.apiBaseUrl}/v1/info`, { params: { action: 'getSubAccountIds', walletAddress: walletAddress } }, { headers: this.headers }); return response.data.response; } // Place orders async placeOrders(subAccountId, orders, grouping = "") { const nonce = this.getNonce(); const expiresAfter = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now (Unix seconds) const types = { PlaceOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orders", type: "Order[]" }, { name: "grouping", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ], Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "orderType", type: "string" }, { name: "price", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "quantity", type: "string" }, { name: "reduceOnly", type: "bool" }, { name: "isTriggerMarket", type: "bool" }, { name: "clientOrderId", type: "string" }, { name: "closePosition", type: "bool" } ] }; const message = { subAccountId: BigInt(subAccountId), orders, grouping, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const signature = await this.wallet.signTypedData(this.domain, types, message); const { v, r, s } = ethers.Signature.from(signature); const response = await axios.post(`${this.apiBaseUrl}/v1/trade`, { params: { action: "placeOrders", subAccountId: subAccountId, orders, grouping }, nonce, signature: { v, r, s }, expiresAfter }, { headers: this.headers }); return response.data.response; } } // Usage async function main() { const client = new SynthetixClient(PRIVATE_KEY); // Get subaccount IDs and use the master (first) subaccount const subAccountIds = await client.getSubAccountIds(client.wallet.address); if (subAccountIds.length === 0) { throw new Error('No subaccounts found. Please fund your account via the app first.'); } const subAccountId = subAccountIds[0]; // Use master subaccount console.log(`Using subaccount: ${subAccountId}`); // Place a limit order const result = await client.placeOrders(subAccountId, [{ symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", price: "50000.00", triggerPrice: "", quantity: "0.01", reduceOnly: false, isTriggerMarket: false, clientOrderId: "", closePosition: false }]); console.log('Order placed:', result); } main().catch(console.error); ``` #### Python ```bash pip install eth-account requests ``` ```python import os import time from eth_account import Account from eth_account.messages import encode_structured_data import requests class SynthetixClient: def __init__(self, private_key): self.account = Account.from_key(private_key) # API base URL self.api_base_url = "https://papi.synthetix.io" # Required headers self.headers = {"User-Agent": "my-trading-bot/1.0.0"} # EIP-712 domain - chainId matches the blockchain network self.domain = { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" } def get_nonce(self): return int(time.time() * 1000) def sign_typed_data(self, primary_type, types, message): typed_data = { "types": { "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"} ], **types }, "primaryType": primary_type, "domain": self.domain, "message": message } encoded = encode_structured_data(typed_data) signed = self.account.sign_message(encoded) return {"v": signed.v, "r": hex(signed.r), "s": hex(signed.s)} def get_subaccount_ids(self, wallet_address): response = requests.post( f"{self.api_base_url}/v1/info", json={"params": {"action": "getSubAccountIds", "walletAddress": wallet_address}}, headers=self.headers ) return response.json()["response"] def place_orders(self, sub_account_id, orders, grouping=""): nonce = self.get_nonce() expires_after = int(time.time()) + 3600 # 1 hour from now (Unix seconds) types = { "PlaceOrders": [ {"name": "subAccountId", "type": "uint256"}, {"name": "orders", "type": "Order[]"}, {"name": "grouping", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ], "Order": [ {"name": "symbol", "type": "string"}, {"name": "side", "type": "string"}, {"name": "orderType", "type": "string"}, {"name": "price", "type": "string"}, {"name": "triggerPrice", "type": "string"}, {"name": "quantity", "type": "string"}, {"name": "reduceOnly", "type": "bool"}, {"name": "isTriggerMarket", "type": "bool"}, {"name": "clientOrderId", "type": "string"}, {"name": "closePosition", "type": "bool"} ] } message = { "subAccountId": int(sub_account_id), "orders": orders, "grouping": grouping, "nonce": nonce, "expiresAfter": expires_after } signature = self.sign_typed_data("PlaceOrders", types, message) response = requests.post( f"{self.api_base_url}/v1/trade", json={ "params": { "action": "placeOrders", "subAccountId": str(sub_account_id), "orders": orders, "grouping": grouping }, "nonce": nonce, "signature": signature, "expiresAfter": expires_after }, headers=self.headers ) return response.json()["response"] # Usage client = SynthetixClient(os.getenv("PRIVATE_KEY")) # Get subaccount IDs and use the master (first) subaccount subaccount_ids = client.get_subaccount_ids(client.account.address) if not subaccount_ids: raise Exception("No subaccounts found. Please fund your account via the app first.") sub_account_id = subaccount_ids[0] # Use master subaccount print(f"Using subaccount: {sub_account_id}") # Place order result = client.place_orders(sub_account_id, [{ "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "quantity": "0.01", "reduceOnly": False, "isTriggerMarket": False, "clientOrderId": "", "closePosition": False }]) print("Order placed:", result) ``` ### Additional Order Examples ```javascript // Market order { orderType: "market", price: "", triggerPrice: "" } // Limit IOC (Immediate or Cancel) { orderType: "limitIoc", price: "50000.00", triggerPrice: "" } // Stop Loss (market execution on trigger) { orderType: "triggerSl", price: "", triggerPrice: "45000.00", isTriggerMarket: true } // Take Profit (limit execution on trigger) { orderType: "triggerTp", price: "55000.00", triggerPrice: "54000.00", isTriggerMarket: false } ``` ### WebSocket Real-Time Updates For real-time order updates and market data streaming, see the [WebSocket API documentation](https://developers.synthetix.io/developer-resources/api/ws-api). Available subscriptions: * **Order updates** - Real-time fills, cancellations, rejections * **Market prices** - Live price feeds * **Orderbook** - Depth updates * **Trades** - Public trade execution events ### Next Steps * **[REST API Reference](https://developers.synthetix.io/developer-resources/api/rest-api)** - Complete endpoint documentation * **[WebSocket Guide](https://developers.synthetix.io/developer-resources/api/ws-api)** - Real-time data streaming * **[Environments](https://developers.synthetix.io/environments)** - Environment configuration * **[Authentication](https://developers.synthetix.io/developer-resources/api/authentication)** - EIP-712 signing details ## Error Handling This guide covers error response shapes, error codes, and retry strategies for the Synthetix API. ### API Error Codes Every order rejection now includes a machine-readable `errorCode` field alongside the human-readable `error` message. Use `errorCode` for programmatic handling; treat `error` as debug context only. ### Response Shapes #### Place / Cancel Orders Trading rejections are per-item inside an HTTP 200 response: ```json { "status": "ok", "response": { "statuses": [ { "resting": { "order": { "venueId": "123", "clientId": "0xabc" }, "id": "123" } }, { "error": "reduce-only order requires open position", "errorCode": "REDUCE_ONLY_NO_POSITION", "order": { "venueId": "0", "clientId": "0xdef" } } ] }, "timestamp": 1773351037815 } ``` #### Modify Order Rejections are on the response object directly: ```json { "status": "ok", "response": { "order": { "venueId": "456", "clientId": "0xabc" }, "orderId": "456", "status": "rejected", "error": "order 456 not found", "errorCode": "ORDER_NOT_FOUND", "timestamp": 1773351037815 }, "timestamp": 1773351037815 } ``` #### Request Validation Errors Invalid requests return HTTP 400 with a top-level error (no `errorCode` per-item): ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "category": "REQUEST", "message": "orders array cannot be empty", "retryable": false }, "timestamp": 1773351037815 } ``` ### Trading Error Codes Returned as per-item rejections with HTTP 200. | Code | Description | Retryable | | ---------------------------- | --------------------------------------------------------------------------------------------------------------- | --------- | | `CANCEL_FAILED` | Cancel operation failed at the matching engine | No | | `FOK_NOT_FILLED` | Fill-or-kill order could not be fully filled immediately | No | | `IDEMPOTENCY_CONFLICT` | Duplicate `clientOrderId` | No | | `INSUFFICIENT_MARGIN` | Not enough margin for the order | No | | `INVALID_ORDER_SIDE` | Invalid order side | No | | `INVALID_TRIGGER_PRICE` | Trigger price invalid for order type | No | | `IOC_NOT_FILLED` | Immediate-or-cancel found no immediate execution | No | | `MARKET_CLOSED` | Market is not open for trading | No | | `MARKET_NOT_FOUND` | Market does not exist | No | | `MAX_ORDERS_PER_MARKET` | Per-market open order cap reached — cancel orders on this market or see [Tiers](https://developers.synthetix.io/developer-resources/api/tiers) | No | | `MAX_TOTAL_ORDERS` | Global open order cap reached across all markets — cancel orders or see [Tiers](https://developers.synthetix.io/developer-resources/api/tiers) | No | | `MAX_SUB_ACCOUNTS_EXCEEDED` | Subaccount limit reached | No | | `NO_LIQUIDITY` | Market order found no liquidity on the opposing side | No | | `OI_CAP_EXCEEDED` | Order would exceed open interest cap | No | | `OPERATION_TIMEOUT` | Operation timed out | Yes | | `ORDER_NOT_FOUND` | Order does not exist (cancel/modify) | No | | `ORDER_REJECTED_BY_ENGINE` | Matching engine rejection with no specific code (see `error` for details) | No | | `POSITION_NOT_FOUND` | Position does not exist | No | | `POST_ONLY_WOULD_TRADE` | Post-only order would take liquidity | No | | `PRICE_OUT_OF_BOUNDS` | Fill price exceeded configured cap/floor | No | | `QUANTITY_BELOW_FILLED` | Modified quantity is below already-filled amount | No | | `QUANTITY_TOO_SMALL` | Quantity below market minimum | No | | `REDUCE_ONLY_NO_POSITION` | Reduce-only order but no position exists | No | | `REDUCE_ONLY_SAME_SIDE` | Reduce-only order on the same side as position | No | | `REDUCE_ONLY_WOULD_INCREASE` | Reduce-only order would increase position size | No | | `SELF_TRADE_PREVENTED` | Order would match against own resting order | No | | `WICK_INSURANCE_ACTIVE` | Wick protection period active, try again shortly | Yes | ### Request / System Error Codes Returned with HTTP 4xx/5xx as top-level errors. | Code | HTTP | Retryable | | ------------------------ | ---- | --------- | | `VALIDATION_ERROR` | 400 | No | | `MISSING_REQUIRED_FIELD` | 400 | No | | `INVALID_FORMAT` | 400 | No | | `INVALID_VALUE` | 400 | No | | `UNAUTHORIZED` | 401 | No | | `FORBIDDEN` | 403 | No | | `NOT_FOUND` | 404 | No | | `METHOD_NOT_ALLOWED` | 405 | No | | `RATE_LIMIT_EXCEEDED` | 429 | Yes | | `INTERNAL_ERROR` | 500 | Yes | | `DATABASE_ERROR` | 500 | Yes | | `CACHE_ERROR` | 500 | Yes | | `UNKNOWN_ERROR` | 500 | No | ### Retry Policy Only three codes should be retried: | Code | Strategy | | ----------------------- | --------------------------------------- | | `WICK_INSURANCE_ACTIVE` | Wait a few seconds, retry unchanged | | `OPERATION_TIMEOUT` | Retry immediately or with short backoff | | `RATE_LIMIT_EXCEEDED` | Backoff per rate limit headers | All other codes indicate a condition that won't resolve without changing the request or account state. ### Batch Request Error Handling Batch operations like `placeOrders` and `cancelOrders` return HTTP 200 with per-item statuses. Each item in the `statuses` array corresponds to the item at the same index in your request. #### Key Points * **Order preserved**: Status array indices match the order of items in your request * **Independent processing**: Each item is validated independently * **No rollback**: Successfully processed items are not rolled back if others fail * **Check every item**: An HTTP 200 response does not mean all items succeeded #### Handling Batch Responses Always iterate through the `statuses` array and check for `errorCode` on each item: ```javascript function handleBatchResponse(response) { if (response.status === 'error') { // Request-level error - entire batch failed throw new Error(response.error.message); } const results = { succeeded: [], failed: [] }; for (const status of response.response.statuses) { if (status.errorCode) { results.failed.push({ errorCode: status.errorCode, error: status.error }); } else if (status.resting) { results.succeeded.push({ type: 'resting', id: status.resting.id }); } else if (status.filled) { results.succeeded.push({ type: 'filled', ...status.filled }); } else if (status.canceled) { results.succeeded.push({ type: 'canceled', id: status.canceled.id }); } } return results; } ``` ### WebSocket Error Handling WebSocket connections use a different error format for responses: ```json { "id": "request-123", "requestId": "request-123", "status": 400, "timestamp": 1704067200000, "traceId": "abc123def456", "error": { "errorCode": "VALIDATION_ERROR", "code": 400, "message": "Invalid order parameters", "category": "REQUEST", "retryable": false, "details": {} } } ``` WebSocket error fields: * `errorCode` - Specific error code for programmatic handling * `category` - Error category (REQUEST, AUTH, RATE\_LIMIT, TRADING, SYSTEM) * `retryable` - Whether the operation can be retried * `details` - Additional context about the error WebSocket-specific close codes: * Connection errors result in WebSocket close codes * Authentication failures close the connection with code 1008 * Protocol errors use code 1002 ## Trading Fees Fee structure for perpetual futures trading on Synthetix. ### Fee Schedule Synthetix uses a tiered fee structure based on 14-day trading volume. All fees are calculated as a percentage of notional trade value. Your tier also affects open order caps and subaccount limits — see [Tiers](https://developers.synthetix.io/developer-resources/api/tiers) for the full breakdown. #### Fee Tiers | Tier | Min 14-Day Volume | Maker Fee | Taker Fee | | ------------ | ----------------- | --------- | --------- | | Regular User | $0 | 0.020% | 0.050% | | Tier 1 | $100,000 | 0.020% | 0.050% | | Tier 2 | $5,000,000 | 0.016% | 0.040% | | Tier 3 | $25,000,000 | 0.014% | 0.035% | | Tier 4 | $100,000,000 | 0.012% | 0.032% | | Tier 5 | $500,000,000 | 0.008% | 0.025% | | Tier 6 | $2,000,000,000 | 0.003% | 0.020% | | Tier 7 | $5,000,000,000 | 0.000% | 0.017% | ### Fee Types #### Maker Fees Applied when your order adds liquidity to the order book: * Limit orders that rest on the book before filling * Post-only orders (`limitGtc` with `postOnly: true`) #### Taker Fees Applied when your order removes liquidity from the order book: * Market orders * Limit orders that fill immediately * Stop loss and take profit orders that execute ### Checking Your Fee Rate Use the [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) endpoint to retrieve your current fee rates. The response includes a `feeRates` object with your maker fee rate, taker fee rate, and tier name. ### Fee Calculation Fees are calculated on the notional value of each trade: ``` Fee Amount = Trade Size × Price × Fee Rate ``` #### Example Calculation For a trade of 1 BTC at $50,000 with a 0.05% taker fee: ``` Notional Value = 1 BTC × $50,000 = $50,000 Fee = $50,000 × 0.0005 = $25 ``` ### Implementation Notes * Tier status updates automatically based on rolling 14-day volume * Fees deducted from account collateral balance * All fees shown in USDT * Volume calculated across all markets ### Next Steps * [Tiers](https://developers.synthetix.io/developer-resources/api/tiers) - Full tier table including order limits and subaccount caps * [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - View individual trade fees in execution history * [Market Makers](https://developers.synthetix.io/developer-resources/api/market-makers) - MM-specific information ## General Information ### Overview The Synthetix API provides access to perpetual futures trading on our trading platform. The API is designed with high performance and security in mind, offering both REST and WebSocket endpoints for different use cases. Most actions are available on both REST and WS, with some additional subscriptions available through our WebSocket. For connection URLs and environment configuration, see [Environments](https://developers.synthetix.io/environments). ### API Architecture The Synthetix API follows a unified design pattern: * **Two REST endpoints**: * `/v1/trade` - Trading operations and account management (autheticated) * `/v1/info` - Public market data and exchange information (no authentication required) * **WebSocket endpoints**: * `/v1/ws/trade` - Trading operations * `/v1/ws/info` - Public market data ### Request Format All REST API requests: * For REST requests, uses **POST** method exclusively * Accept **JSON** request bodies * Return **JSON** responses * Include API version in the URL path ### Authentication All trading actions require **EIP-712 signatures** for authentication. This provides cryptographic proof that requests originate from the account owner without requiring traditional API keys. * No API keys required * Each request is signed with your wallet's private key * Supports subaccount delegation for authorized trading * See [Authentication](https://developers.synthetix.io/developer-resources/api/rest-api/trade/authentication) for detailed information ### Rate Limiting API requests are rate-limited to ensure fair usage and system stability. Detailed rate limit specifications will be published closer to mainnet launch. See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for more information. ### Data Types * **Amounts and Prices**: All numeric values are represented as strings to preserve precision * **Timestamps**: Unix timestamps in milliseconds * **Order IDs**: 64-bit integers * **Client Order IDs**: 128-bit hex strings (0x prefix + 32 hex characters) ### Response Format All API responses follow a consistent format: #### Success Response ```json { "status": "ok", "response": [ // method specific response object or array. For example, when calling getOrders { "orderId": "1958787130134106112", "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "quantity": "10", "price": "45000", "createdTime": 1755846234, "updatedTime": 1755846234, "filledQuantity": "10", "filledPrice": "45000" } ], "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` #### Error Response When an error occurs, the `status` will be set to `error`, a `response` will not be included, and any additional `error` information will be included instead. ```json { "status": "error", "error": { "code": "INTERNAL_ERROR", "message": "Failed to list subaccounts", "category": "SYSTEM", "retryable": false, "details": { /* optional context */ } }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` Error responses include: * `code` - Specific error code for programmatic handling * `category` - Error classification (REQUEST, AUTH, RATE\_LIMIT, TRADING, SYSTEM) * `retryable` - Whether the request can be retried * `traceId` - Trace ID for debugging and support See [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) for complete error documentation. ### Getting Started 1. Choose your integration method: * **REST API** for request-response operations * **WebSocket API** for real-time data and lower latency trading 2. Set up authentication: * Implement EIP-712 signing for trading endpoints * Public data endpoints require no authentication 3. Handle API errors and rate limit responses ### Next Steps * [REST API](https://developers.synthetix.io/developer-resources/api/rest-api) - REST endpoint documentation * [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) - WebSocket endpoint documentation * [Authentication](https://developers.synthetix.io/developer-resources/api/authentication) - Detailed authentication guide * [Fees](https://developers.synthetix.io/developer-resources/api/fees) - Fee schedule and tier structure ## Market Makers Quick reference for market makers integrating with Synthetix perpetual futures. ### Overview Synthetix provides a trading API with EIP-712 authentication, and WebSocket support for low-latency trading. ### Rate Limits API rate limits apply to ensure fair usage and system stability. See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for more information. ### Order Types | Order Type | Description | Maker/Taker | | ---------- | -------------------------------------------------------- | ------------------------------ | | `limitGtc` | Good Till Canceled (use `postOnly: true` for maker-only) | Either (Maker with `postOnly`) | | `limitIoc` | Immediate or Cancel | Taker | | `market` | Market execution | Taker | **Post-Only (`limitGtc` with `postOnly: true`):** * Rejected if would match immediately * Guarantees maker fee rate * Essential for market making **Reduce-Only:** * Set `reduceOnly: true` to only close positions * Prevents increasing exposure See [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) for complete order type documentation. ### WebSocket API **Recommended Subscriptions:** * `subAccountUpdates` - Consolidated stream for orders, trades, positions, and funding * `orderbook` - Real-time orderbook depth updates * `marketPrices` - Mark, index, and last price updates **Trading Operations:** * Place and cancel orders directly via WebSocket for lower latency * All trading operations require EIP-712 signatures See [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) and [Subscriptions](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions) for complete documentation. ### Authentication * EIP-712 signatures required for all trading operations * No API keys needed * Supports subaccount delegation * Domain: `"Synthetix"`, Version: `"1"`, ChainId: `1` See [Authentication](https://developers.synthetix.io/developer-resources/api/authentication) for implementation details. ### Quick Start 1. Review [fee structure](https://developers.synthetix.io/developer-resources/api/fees) and tier requirements 2. Set up [WebSocket connections](https://developers.synthetix.io/developer-resources/api/ws-api) for low latency 3. Use `limitGtc` with `postOnly: true` for post-only (maker) orders 4. Subscribe to `subAccountUpdates` for consolidated trading updates 5. Implement proper reconnection and heartbeat handling ### API Reference * [Fees](https://developers.synthetix.io/developer-resources/api/fees) - Complete fee tier structure * [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) - Detailed rate limit documentation * [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - Order placement endpoint * [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) - Real-time trading and market data * [Authentication](https://developers.synthetix.io/developer-resources/api/authentication) - EIP-712 signing guide ## API Notation Synthetix API conventions and data formats. ### 1. Naming Conventions The Synthetix API uses **mixed naming conventions** based on field type: | Field Category | Convention | Examples | | ---------------------------------- | -------------- | --------------------------------------------------------------- | | **Business/Domain Fields** | camelCase | `subAccountId`, `clientOrderId`, `orderType`, `walletAddress` | | **System Fields (REST responses)** | snake\_case | `request_id`, `timestamp` (RFC3339 string) | | **System Fields (WebSocket)** | Shortened | `id` (in requests/responses) | | **Methods** | camelCase | `placeOrders`, `cancelOrders`, `getPositions` | | **Notification Methods (WS)** | snake\_case | `market_price_update`, `order_update`, `orderbook_depth_update` | | **Symbols** | Dash-separated | `BTC-USDT`, `ETH-USDT` (format: `BASE-QUOTE`) | #### Field Naming by Protocol | Context | Field | Format | Example | | -------------------- | ---------------- | -------------------------- | ------------------------------------- | | **REST Responses** | Request tracking | `request_id` (snake\_case) | `"request_id": "5ccf215d37e3ae6d"` | | **REST Responses** | Timestamps | RFC3339 string | `"timestamp": "2025-01-01T00:00:00Z"` | | **WebSocket** | Request tracking | `id` | `"id": "request-123"` | | **All Protocols** | Business data | camelCase | `"subAccountId": "123456789"` | | **WS Notifications** | Method names | snake\_case | `"method": "market_price_update"` | ### 2. Data Types #### Numeric Precision **All prices and quantities as strings** to avoid floating-point errors: ```json // ✓ Correct - All numeric values as strings { "price": "50000.50", "quantity": "0.1", "leverage": "10" } // ✗ Wrong - Numeric values as numbers { "price": 50000.50, "quantity": 0.1, "leverage": 10 } ``` #### Timestamps & Nonces **Context matters for timestamp format:** | Context | Format | Example | | --------------------------------- | ------------------------------ | ------------------------------------- | | **Request fields** (nonce) | Positive, incrementing integer | `"nonce": 1704067200000` | | **Request fields** (expiresAfter) | Unix seconds (number) | `"expiresAfter": 1704067260` | | **REST response fields** | RFC3339 string | `"timestamp": "2025-01-01T00:00:00Z"` | | **WebSocket response fields** | Unix milliseconds (number) | `"timestamp": 1704067200000` | ```json // ✓ Correct - Request with Unix timestamps { "nonce": 1704067200000, // Positive integer nonce "expiresAfter": 1704067260 // Unix seconds } // ✓ Correct - REST API response with RFC3339 string { "status": "ok", "response": {...}, "request_id": "5ccf215d37e3ae6d", "timestamp": "2025-01-01T00:00:00Z" // String in REST responses } ``` #### Booleans & Arrays ```json { "reduceOnly": false, // ✓ Boolean "orders": [{...}, {...}], // ✓ Array for batch ops "orders": [] // ✓ Empty arrays valid } ``` ### 3. Identifiers | ID Type | Format | Example | Purpose | | ----------------- | --------------- | ---------------------------------------------- | ---------------------------- | | **subAccountId** | String (uint64) | `"1234567890123456789"` | Trading account | | **orderId** | String (uint64) | `"1948058938469519360"` | System order ID | | **clientOrderId** | `0x` + 32 hex | `"0x1234567890abcdef1234567890abcdef"` | Client tracking | | **request\_id** | 16 hex chars | `"5ccf215d37e3ae6d"` | Server tracking (REST) | | **id** | Client-defined | `"request-123"` | Request tracking (WebSocket) | | **address** | `0x` + 40 hex | `"0x742d35Cc6634C0532925a3b844Bc9e7D1333D6E2"` | Ethereum wallet | #### ID Usage Rules | Operation | Requires subAccountId | Example | | ------------------ | --------------------- | ----------------------------- | | New orders | ✓ Yes | `placeOrders` | | Existing resources | ✗ No | `cancelOrders` with `orderId` | | Public data | ✗ No | `getMarketData` | ### 4. Trading Constructs #### Order Types (Static) ```json // Limit GTC {"orderType": "limitGtc", "price": "50000", "triggerPrice": "", "isTriggerMarket": false} // Limit IOC {"orderType": "limitIoc", "price": "50000", "triggerPrice": "", "isTriggerMarket": false} // Market {"orderType": "market", "price": "", "triggerPrice": "", "isTriggerMarket": false} // Trigger Stop Loss (market on trigger) {"orderType": "triggerSl", "price": "", "triggerPrice": "50000", "isTriggerMarket": true} // Trigger Take Profit (limit on trigger) {"orderType": "triggerTp", "price": "49950", "triggerPrice": "50000", "isTriggerMarket": false} ``` Rules: * `limitGtc`/`limitIoc`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `market`: price=""; `isTriggerMarket=false`; `triggerPrice=""` * `triggerSl`/`triggerTp`: `triggerPrice` required; if `isTriggerMarket=true` then `price=""`, else `price` required #### Time in Force | Value | Description | | ---------- | ----------------------------------- | | `gtc` | Good Till Canceled | | `ioc` | Immediate or Cancel | | `postOnly` | Post-only flag on `limitGtc` orders | #### States **Order States**: `started`, `open`, `partiallyFilled`, `filled`, `cancelled`, `rejected`, `modifying` **Position States**: `open`, `close`, `update` **Side**: `buy`, `sell` ### 5. Request Envelopes #### REST Info Endpoints Wrap all request parameters inside a `params` object and identify the target operation with `params.action`. ```json { "params": { "action": "getMarkets", "symbol": "BTC-USDT", "limit": 100 } } ``` * `params.action` maps to the specific public info method * Additional request fields live alongside `params.action` inside `params` #### REST Trade Endpoints Trading requests wrap method inputs inside `params` while keeping authentication fields at the top level. ```json { "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [{ /* ... */ }] }, "nonce": 1704067200000, "expiresAfter": 1704067260, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` * `params.action` identifies the trade method; additional method params including `subAccountId` live inside `params` * `nonce` is a positive integer (unique per request) and `expiresAfter` is Unix seconds, both at root level * `signature` follows the shared EIP-712 format at root level ### 6. API Mechanics #### EIP-712 Signatures ```json { "signature": { "v": 28, // Recovery ID: 0, 1, 27, or 28 "r": "0x...", // 32 bytes hex "s": "0x..." // 32 bytes hex } } ``` #### Response Format **REST API responses** use snake\_case for system fields: ```json { "status": "ok", // "ok" or "error" "response": {...}, // Success data (omitted on error) "error": { // Error details (omitted on success) "code": "VALIDATION_ERROR", "message": "Description" }, "request_id": "5ccf215d37e3ae6d", // snake_case for REST "timestamp": "2025-01-01T00:00:00Z" // RFC3339 string for REST } ``` **WebSocket responses** use shortened field names: ```json { "id": "request-123", // Matches client's request ID "status": 200, // HTTP-style status code "result": {...} // Success data (omitted on error) } ``` #### Error Codes | Code | HTTP | Description | | ------------------ | ---- | ---------------- | | `VALIDATION_ERROR` | 400 | Invalid request | | `INVALID_FORMAT` | 400 | Incorrect format | | `INVALID_VALUE` | 400 | Out of range | | `NOT_FOUND` | 404 | Resource missing | | `INTERNAL_ERROR` | 500 | System error | ### Quick Reference #### Validation Patterns * **subAccountId**: `^\d{1,19}$` (uint64 as string) * **clientOrderId**: `^0x[a-fA-F0-9]{32}$` * **request\_id**: `^[a-f0-9]{16}$` (REST API responses) * **id**: varies by context (WebSocket client-set) #### Implementation Notes * Omit `null` values - remove optional fields entirely import CollateralObject from '../../../snippets/collateral-object.mdx'; import CrossMarginSummaryObject from '../../../snippets/cross-margin-summary-object.mdx'; import MarketObject from '../../../snippets/market-object.mdx'; import OrderObject from '../../../snippets/order-object.mdx'; import PositionObject from '../../../snippets/position-object.mdx'; import SubaccountObject from '../../../snippets/subaccount-object.mdx'; import TradeObject from '../../../snippets/trade-object.mdx'; ## Objects This page provides detailed reference information for all object types used throughout the Synthetix API. These objects represent the core data structures that you'll encounter when interacting with trading endpoints, account management, and market data. ### Market Object The Market object represents a trading market/pair configuration, including trading rules, size limits, margin requirements, and decimal precision settings. ### Order Object The Order object represents a trading order in the system. Orders can be in various states and contain information about execution, pricing, and timing. ### Position Object The Position object represents an open trading position, including profit/loss calculations, margin requirements, and risk management data. ### Cross Margin Summary Object The Cross Margin Summary object provides comprehensive margin and risk metrics for a subaccount's cross-margin trading activity. ### Subaccount Object The Subaccount object contains account-level information including balances, positions, and trading permissions. ### Trade Object The Trade object represents a completed trade execution, containing details about price, quantity, fees, and timing. ### Collateral Object The Collateral object represents assets that can be used as margin or collateral for trading. ### Object Usage Notes * **Field Naming**: All object field names follow camelCase naming convention (`subAccountId`, `orderId`, `clientOrderId`) * **String Numbers**: Most numeric values are returned as strings to preserve precision and avoid floating-point rounding issues * **Timestamps**: All timestamps are Unix milliseconds (unsigned 64-bit integer) representing time in milliseconds since the Unix epoch (e.g., `1704067200000`) * **IDs**: Object IDs are typically strings or integers depending on the specific object type * **Nullability**: Some fields may be `null` or omitted depending on the object state * **Arrays**: Collection fields (like `positions`, `collaterals`) are always arrays, even when empty ## Rate Limiting This guide explains how rate limits work on the Synthetix off-chain trading APIs and how to build clients that stay within limits. *** ### Overview Rate limits protect the system and ensure fair access for all users. Limits apply per subaccount and, on WebSocket, per IP address. | API | Per-subaccount limit | Per-IP limit | | --------------------------- | ------------------------ | ----------------- | | **REST** (`POST /v1/trade`) | Yes | No | | **WebSocket** | Yes (trade actions only) | Yes (all actions) | *** ### How Limits Work Limits use a **token bucket** model: * Each subaccount (and on WebSocket, each IP) has a bucket of tokens * Each request consumes tokens based on the action * Tokens refill over time * If a request would exceed available tokens, it is rejected with `429 Too Many Requests` #### Rate limit buckets (Mainnet) Token costs are the same across environments. Bucket sizes may differ on Testnet. See [Environments](https://developers.synthetix.io/environments) for environment-specific configuration. | Limit type | Tokens per window | Window | Scope | | -------------------------- | ----------------- | ---------- | -------------------------- | | Per-subaccount (REST) | 1,000 | 10 seconds | `POST /v1/trade` | | Per-subaccount (WebSocket) | 1,000 | 10 seconds | Trade actions only | | Per-IP (WebSocket) | 10,000 | 10 seconds | All actions (trade + info) | #### Token costs — Trade actions (REST and WebSocket subaccount) These costs apply to both REST `POST /v1/trade` and WebSocket trade actions. For `placeOrders`, cost = base cost × number of orders in the batch. | Action | Base cost | Notes | | --------------------------- | --------- | ------------------------------------ | | `addDelegatedSigner` | 100 | | | `cancelAllOrders` | 1 | | | `cancelOrders` | 1 | | | `createSubaccount` | 100 | | | `getBalanceUpdates` | 100 | | | `getDelegatedSigners` | 20 | | | `getDelegationsForDelegate` | 20 | | | `getFeeRate` | 10 | | | `getFees` | 10 | | | `getFundingPayments` | 100 | | | `getOpenOrders` | 10 | | | `getOrderHistory` | 50 | | | `getPerformanceHistory` | 100 | | | `getPortfolio` | 10 | | | `getPositions` | 10 | | | `getRateLimits` | 20 | | | `getReferral` | 20 | | | `getSubAccount` | 20 | | | `getSubAccounts` | 20 | | | `getTrades` | 20 | | | `getTransfers` | 10 | | | `modifyOrder` | 4 | | | `modifyOrderBatch` | 4 | | | `placeIsolatedOrder` | 4 | | | `placeOrders` | 4 | Cost = 4 × number of orders in batch | | `removeAllDelegatedSigners` | 100 | | | `removeDelegatedSigner` | 100 | | | `scheduleCancel` | 4 | | | `transferCollateral` | 5 | | | `updateIsolatedMargin` | 5 | | | `updateLeverage` | 5 | | | `updateSubAccountName` | 100 | | | `voluntaryAutoExchange` | 100 | | | `withdrawCollateral` | 5 | | #### Token costs — WebSocket info actions (per-IP limit only) These costs apply only to the per-IP bucket on WebSocket. Info actions do not consume from the per-subaccount bucket. | Action | Cost | | ----------------------- | ----- | | `getCandles` | 200 | | `getCollaterals` | 50 | | `getFundingRate` | 250 | | `getFundingRateHistory` | 1,000 | | `getIsWhitelisted` | 250 | | `getLastTrades` | 200 | | `getMarketPrices` | 200 | | `getMarkets` | 50 | | `getMids` | 50 | | `getOpenInterest` | 50 | | `getOrderbook` | 200 | | `getSubAccountIds` | 250 | Trade actions on WebSocket also consume from the per-IP bucket using the same costs as the trade table above. *** ### REST API * **Endpoint:** `POST /v1/trade` * **Limit:** Per authenticated subaccount * **Scope:** All trade actions *** ### WebSocket API Two limits apply: 1. **Per-IP limit** — Applies to all actions (trade and info). Checked first. 2. **Per-subaccount limit** — Applies to trade actions only. Both must pass for a trade action. Multiple WebSocket connections from the same IP share the same per-IP budget. *** ### Rate Limit Response When you exceed a limit, the API returns: * **HTTP status:** `429 Too Many Requests` * **Error code:** `RATE_LIMIT_EXCEEDED` * **Category:** `RATE_LIMIT` * **Retryable:** `true` #### REST example ```json { "success": false, "clientRequestId": "abc-123", "error": { "code": "RATE_LIMIT_EXCEEDED", "category": "RATE_LIMIT", "message": "Rate limit exceeded for action 'placeOrders'", "retryable": true } } ``` #### WebSocket example (subaccount limit) ```json { "success": false, "clientRequestId": "abc-123", "error": { "code": "RATE_LIMIT_EXCEEDED", "category": "RATE_LIMIT", "message": "Rate limit exceeded for action 'placeOrders'" } } ``` #### WebSocket example (IP limit) ```json { "success": false, "clientRequestId": "abc-123", "error": { "code": "RATE_LIMIT_EXCEEDED", "category": "RATE_LIMIT", "message": "IP rate limit exceeded" } } ``` *** ### Implementation Notes for Bots #### 1. Use exponential backoff on 429 When you receive `429` with `RATE_LIMIT_EXCEEDED`, wait before retrying. Start with about 1 second and increase on repeated failures (e.g. 1s, 2s, 4s). #### 2. Pace `placeOrders` requests Each order in a batch consumes 4 tokens (cost = 4 × batch size). A batch of 20 orders consumes 80 tokens. With a 1,000-token subaccount limit per 10 seconds, large batches can hit limits quickly. Consider: * Smaller batches * Spacing requests over time * Avoiding bursts of many orders at once #### 3. Share IP budget on WebSocket Per-IP limits apply across all WebSocket connections from the same IP. If you run multiple bots or connections from one server, they share the same IP budget. #### 4. Treat rate limits as retryable Rate limit errors are marked `retryable: true`. Retry after a short delay instead of treating them as permanent failures. #### 5. Avoid hammering after a 429 Back off and reduce request rate after hitting a limit. Continuing at the same rate will keep triggering 429s. *** ### Related Documentation * [Environments](https://developers.synthetix.io/environments) - Environment-specific configuration * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Complete error handling guide * [General Information](https://developers.synthetix.io/developer-resources/api/general-information) - API overview * [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - Order placement endpoint * [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) - WebSocket connection documentation ## Tiers Synthetix uses a volume-based tier system. A subaccount's tier determines its trading fee rates, open order caps, and the number of subaccounts the wallet can create. ### Tier table | Tier | Min 14-day Volume | Maker fee | Taker fee | Max orders per market | Max total open orders | Max subaccounts | | ------------ | ----------------- | --------- | --------- | --------------------- | --------------------- | --------------- | | Regular User | $0 | 0.020% | 0.050% | 10 | 50 | 1 | | Tier 1 | $100,000 | 0.020% | 0.050% | 15 | 75 | 5 | | Tier 2 | $5,000,000 | 0.016% | 0.040% | 25 | 150 | 10 | | Tier 3 | $25,000,000 | 0.014% | 0.035% | 50 | 300 | 10 | | Tier 4 | $100,000,000 | 0.012% | 0.032% | 100 | 500 | 10 | | Tier 5 | $500,000,000 | 0.008% | 0.025% | 150 | 700 | 10 | | Tier 6 | $2,000,000,000 | 0.003% | 0.020% | 200 | 1,000 | 10 | | Tier 7 | $5,000,000,000 | 0.000% | 0.017% | 200 | 1,000 | 10 | Volume is measured in notional USDT traded across all markets in a rolling 14-day window. Tier assignments update automatically when the volume threshold is crossed — no API action is required. ### Open order limits Two separate caps apply to each subaccount simultaneously: * **`max_orders_per_market`** — maximum open orders on a single market symbol * **`max_total_orders`** — maximum open orders across all markets combined **What counts toward the limit:** * All resting limit orders * Conditional orders (take-profit, stop-loss) — each counts as one slot **What does not count:** * Reduce-only orders * Liquidation orders placed by the engine Exceeding either cap causes `placeOrders` to return an error for the affected order: | Error code | Meaning | | ----------------------- | ------------------------------------- | | `MAX_TOTAL_ORDERS` | Global cap across all markets reached | | `MAX_ORDERS_PER_MARKET` | Per-market cap reached | Both codes are non-retryable. Resolution: cancel open orders to free slots, or wait for the account's tier to increase. See [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling). ### Limits update live When a subaccount's tier changes (due to volume crossing a threshold or a manual tier assignment), the new limits take effect immediately for subsequent orders. Existing open orders are not cancelled. ### Checking current tier The [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) endpoint returns the subaccount's current `feeRates` object, which includes the tier name and current maker/taker rates. ## EIP-712 Signing Implementation This guide provides detailed implementation instructions for EIP-712 signature generation across different programming languages and libraries. ### Core Concepts #### Domain Separator The domain separator for off-chain authentication (uses zero address as verifying contract): ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; ``` #### Message Types Each API method has its own EIP-712 type definition. Here are the actual implementations: ##### WebSocket Authentication ```javascript const authTypes = { AuthMessage: [ { name: "subAccountId", type: "uint256" }, { name: "timestamp", type: "uint256" }, { name: "action", type: "string" } ] }; // Example message const authMessage = { subAccountId: "123456789", // Your subaccount ID timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in seconds action: "websocket_auth" }; ``` ##### Place Orders ```javascript const placeOrderTypes = { Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "orderType", type: "string" }, { name: "price", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "quantity", type: "string" }, { name: "reduceOnly", type: "bool" }, { name: "isTriggerMarket", type: "bool" }, { name: "clientOrderId", type: "string" }, { name: "closePosition", type: "bool" } ], PlaceOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orders", type: "Order[]" }, { name: "grouping", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; // Order type values — plain strings, no JSON.stringify needed const orderTypes = { limitGtc: "limitGtc", // Limit order, Good Till Canceled limitIoc: "limitIoc", // Limit order, Immediate or Cancel market: "market", // Market order triggerSl: "triggerSl", // Trigger Stop Loss triggerTp: "triggerTp" // Trigger Take Profit }; // Example usage in order: const order = { symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", // Plain string value price: "50000.00", // Required for limit orders, empty string for market triggerPrice: "", // Required for trigger orders, empty string otherwise quantity: "0.1", reduceOnly: false, isTriggerMarket: false, // For trigger orders: true = market execution clientOrderId: "0x1234567890abcdef1234567890abcdef", closePosition: false }; ``` ##### Cancel Orders ```javascript const cancelOrderTypes = { CancelOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orderIds", type: "uint256[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### Cancel All Orders ```javascript const cancelAllOrderTypes = { CancelAllOrders: [ { name: "subAccountId", type: "uint256" }, { name: "symbols", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### Modify Order ```javascript const modifyOrderTypes = { ModifyOrder: [ { name: "subAccountId", type: "uint256" }, { name: "orderId", type: "uint256" }, { name: "price", type: "string" }, { name: "quantity", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### Update Leverage ```javascript const updateLeverageTypes = { UpdateLeverage: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "leverage", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### Create Subaccount ```javascript const createSubaccountTypes = { CreateSubaccount: [ { name: "masterSubAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; // Note: masterSubAccountId is the ID of an existing subaccount you own. // It proves ownership and links the new subaccount to your master account. ``` ##### Update SubAccount Name ```javascript const updateSubAccountNameTypes = { UpdateSubAccountName: [ { name: "subAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### Add Delegated Signer ```javascript const addDelegatedSignerTypes = { AddDelegatedSigner: [ { name: "delegateAddress", type: "address" }, { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" }, { name: "expiresAt", type: "uint256" }, { name: "permissions", type: "string[]" } ] }; // expiresAt: when the delegation permission expires (0 = no expiration) // expiresAfter: when this signing request expires // permissions: use ["session"] for trading access or ["delegate"] for trading access plus session delegation ``` ##### Remove All Delegated Signers ```javascript const removeAllDelegatedSignersTypes = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; ``` ##### SubAccount Actions Used for: `getPositions`, `getOpenOrders`, `getOrderHistory`, `getTrades`, `getFundingPayments`, `getSubAccount`, `getDelegatedSigners`, `getBalanceUpdates` ```javascript const subAccountActionTypes = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; ``` #### Nonce The `nonce` field is a replay-protection value required for all state-changing actions (any action not starting with `get`). Each nonce must be a positive integer, incrementing and unique per request. **Recommended generation approaches:** * Cryptographically random bytes converted to a uint256 (reference implementation approach) * `Date.now()` also works as a convenient non-zero unique value The nonce is not validated as a timestamp — it only needs to be a positive integer, incrementing and unique per request. #### expiresAfter The `expiresAfter` field is an optional Unix timestamp **in milliseconds** that sets when the request expires. Use `0` to indicate no expiration. ```javascript const expiresAfter = Date.now() + 300000; // 5 minutes from now (milliseconds) ``` ### JavaScript/TypeScript Implementation #### Using viem ```typescript import { createWalletClient, http, parseSignature } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { mainnet } from 'viem/chains'; import crypto from 'crypto'; // Signature validation function function validateSignature(signature: { v: number; r: string; s: string }) { if (!signature || typeof signature !== 'object') { throw new Error('Invalid signature object'); } // Validate v - must be 0, 1, 27, or 28 if (![0, 1, 27, 28].includes(signature.v)) { throw new Error(`Invalid v value: ${signature.v}. Must be 0, 1, 27, or 28`); } // Validate r - must be 32-byte hex string if (!signature.r || !/^0x[a-fA-F0-9]{64}$/.test(signature.r)) { throw new Error('Invalid r value: must be 0x-prefixed 32-byte hex string'); } // Validate s - must be 32-byte hex string if (!signature.s || !/^0x[a-fA-F0-9]{64}$/.test(signature.s)) { throw new Error('Invalid s value: must be 0x-prefixed 32-byte hex string'); } return true; } class SynthetixSigner { private account: any; private client: any; private domain: any; constructor(privateKey: `0x${string}`, chainId = 1) { this.account = privateKeyToAccount(privateKey); this.client = createWalletClient({ account: this.account, chain: chainId === 1 ? mainnet : mainnet, // Add other chains as needed transport: http() }); this.domain = { name: "Synthetix", version: "1", chainId: BigInt(chainId), verifyingContract: "0x0000000000000000000000000000000000000000" }; } async signWebSocketAuth(subAccountId: string, timestamp?: number) { const types = { AuthMessage: [ { name: "subAccountId", type: "uint256" }, { name: "timestamp", type: "uint256" }, { name: "action", type: "string" } ] }; const message = { subAccountId: BigInt(subAccountId), timestamp: BigInt(timestamp || Math.floor(Date.now() / 1000)), // Unix seconds action: "websocket_auth" }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "AuthMessage", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signPlaceOrder(orderData: { subAccountId: string; orders: any[]; grouping: string; nonce?: number; expiresAfter?: number; }) { const types = { Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "orderType", type: "string" }, { name: "price", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "quantity", type: "string" }, { name: "reduceOnly", type: "bool" }, { name: "isTriggerMarket", type: "bool" }, { name: "clientOrderId", type: "string" }, { name: "closePosition", type: "bool" } ], PlaceOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orders", type: "Order[]" }, { name: "grouping", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(orderData.subAccountId), orders: orderData.orders, grouping: orderData.grouping, nonce: BigInt(orderData.nonce || Date.now()), expiresAfter: BigInt(orderData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "PlaceOrders", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signCancelOrder(cancelData: { subAccountId: string; orderIds: string[]; nonce?: number; expiresAfter?: number; }) { const types = { CancelOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orderIds", type: "uint256[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(cancelData.subAccountId), orderIds: cancelData.orderIds.map(id => BigInt(id)), nonce: BigInt(cancelData.nonce || Date.now()), expiresAfter: BigInt(cancelData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "CancelOrders", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signCancelAllOrders(cancelAllData: { subAccountId: string; symbols: string[]; nonce?: number; expiresAfter?: number; }) { const types = { CancelAllOrders: [ { name: "subAccountId", type: "uint256" }, { name: "symbols", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(cancelAllData.subAccountId), symbols: cancelAllData.symbols, nonce: BigInt(cancelAllData.nonce || Date.now()), expiresAfter: BigInt(cancelAllData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "CancelAllOrders", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signModifyOrder(modifyData: { subAccountId: string; orderId: string; price?: string; quantity?: string; triggerPrice?: string; nonce?: number; expiresAfter?: number; }) { const types = { ModifyOrder: [ { name: "subAccountId", type: "uint256" }, { name: "orderId", type: "uint256" }, { name: "price", type: "string" }, { name: "quantity", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(modifyData.subAccountId), orderId: BigInt(modifyData.orderId), price: modifyData.price || "", quantity: modifyData.quantity || "", triggerPrice: modifyData.triggerPrice || "", nonce: BigInt(modifyData.nonce || Date.now()), expiresAfter: BigInt(modifyData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "ModifyOrder", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signUpdateLeverage(leverageData: { subAccountId: string; symbol: string; leverage: string; nonce?: number; expiresAfter?: number; }) { const types = { UpdateLeverage: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "leverage", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(leverageData.subAccountId), symbol: leverageData.symbol, leverage: leverageData.leverage, nonce: BigInt(leverageData.nonce || Date.now()), expiresAfter: BigInt(leverageData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "UpdateLeverage", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signCreateSubaccount(createData: { masterSubAccountId: string; name: string; nonce?: number; expiresAfter?: number; }) { const types = { CreateSubaccount: [ { name: "masterSubAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { masterSubAccountId: BigInt(createData.masterSubAccountId), name: createData.name, nonce: BigInt(createData.nonce || Date.now()), expiresAfter: BigInt(createData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "CreateSubaccount", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signUpdateSubAccountName(updateData: { subAccountId: string; name: string; nonce?: number; expiresAfter?: number; }) { const types = { UpdateSubAccountName: [ { name: "subAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(updateData.subAccountId), name: updateData.name, nonce: BigInt(updateData.nonce || Date.now()), expiresAfter: BigInt(updateData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "UpdateSubAccountName", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signAddDelegatedSigner(delegateData: { delegateAddress: string; subAccountId: string; nonce?: number; expiresAfter?: number; expiresAt?: number; permissions: string[]; }) { const types = { AddDelegatedSigner: [ { name: "delegateAddress", type: "address" }, { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" }, { name: "expiresAt", type: "uint256" }, { name: "permissions", type: "string[]" } ] }; const message = { delegateAddress: delegateData.delegateAddress, subAccountId: BigInt(delegateData.subAccountId), nonce: BigInt(delegateData.nonce || Date.now()), expiresAfter: BigInt(delegateData.expiresAfter || 0), expiresAt: BigInt(delegateData.expiresAt || 0), permissions: delegateData.permissions }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "AddDelegatedSigner", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signRemoveAllDelegatedSigners(removeData: { subAccountId: string; nonce?: number; expiresAfter?: number; }) { const types = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(removeData.subAccountId), nonce: BigInt(removeData.nonce || Date.now()), expiresAfter: BigInt(removeData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "RemoveAllDelegatedSigners", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } async signSubAccountAction(actionData: { subAccountId: string; action: string; expiresAfter?: number; }) { const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(actionData.subAccountId), action: actionData.action, expiresAfter: BigInt(actionData.expiresAfter || 0) }; const signature = await this.client.signTypedData({ domain: this.domain, types, primaryType: "SubAccountAction", message }); const parsedSig = parseSignature(signature); validateSignature(parsedSig); return parsedSig; } } // Usage Example async function example() { const signer = new SynthetixSigner("0x..." as `0x${string}`); // WebSocket authentication (timestamp in seconds) const authSig = await signer.signWebSocketAuth("123456789"); console.log('Auth signature:', authSig); // Place orders const orderSig = await signer.signPlaceOrder({ subAccountId: "123456789", orders: [{ symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", price: "50000", triggerPrice: "", quantity: "0.1", reduceOnly: false, isTriggerMarket: false, clientOrderId: "0x1234567890abcdef1234567890abcdef", closePosition: false }], grouping: "na", nonce: Date.now() }); console.log('Order signature:', orderSig); // Cancel orders const cancelSig = await signer.signCancelOrder({ subAccountId: "123456789", orderIds: ["987654321", "987654322"], nonce: Date.now() }); console.log('Cancel signature:', cancelSig); // Cancel all orders for a symbol const cancelAllSig = await signer.signCancelAllOrders({ subAccountId: "123456789", symbol: "BTC-USDT", nonce: Date.now() }); console.log('Cancel all signature:', cancelAllSig); // Update leverage const leverageSig = await signer.signUpdateLeverage({ subAccountId: "123456789", symbol: "BTC-USDT", leverage: "20", nonce: Date.now() }); console.log('Update leverage signature:', leverageSig); // Create subaccount const createSig = await signer.signCreateSubaccount({ masterSubAccountId: "123456789", // An existing subaccount you own name: "New Trading Account", nonce: Date.now() }); console.log('Create subaccount signature:', createSig); // Update subaccount name const renameSig = await signer.signUpdateSubAccountName({ subAccountId: "123456789", name: "Renamed Account", nonce: Date.now() }); console.log('Update subaccount name signature:', renameSig); // Add delegated signer const addDelegateSig = await signer.signAddDelegatedSigner({ delegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", subAccountId: "123456789", permissions: ["session"], nonce: Date.now() }); console.log('Add delegated signer signature:', addDelegateSig); // Remove all delegated signers const removeAllSig = await signer.signRemoveAllDelegatedSigners({ subAccountId: "123456789", nonce: Date.now() }); console.log('Remove all delegated signers signature:', removeAllSig); // Get positions (no nonce required for SubAccountAction) const positionsSig = await signer.signSubAccountAction({ subAccountId: "123456789", action: "getPositions", expiresAfter: Date.now() + 300000 // Optional: 5 minute expiration (milliseconds) }); console.log('Positions signature:', positionsSig); } ``` ### Python Implementation #### Using web3.py and eth\_account ```python from eth_account.messages import encode_structured_data from eth_account import Account import json import time import re import os def validate_signature(signature): """Validate EIP-712 signature components""" if not isinstance(signature, dict): raise ValueError("Invalid signature object") # Validate v - must be 0, 1, 27, or 28 v = signature.get('v') if v not in [0, 1, 27, 28]: raise ValueError(f"Invalid v value: {v}. Must be 0, 1, 27, or 28") # Validate r - must be 32-byte hex string r_val = signature.get('r') if not r_val or not re.match(r'^0x[a-fA-F0-9]{64}$', r_val): raise ValueError("Invalid r value: must be 0x-prefixed 32-byte hex string") # Validate s - must be 32-byte hex string s_val = signature.get('s') if not s_val or not re.match(r'^0x[a-fA-F0-9]{64}$', s_val): raise ValueError("Invalid s value: must be 0x-prefixed 32-byte hex string") return True DOMAIN_FIELDS = [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"} ] class SynthetixSigner: def __init__(self, private_key, chain_id=1): self.account = Account.from_key(private_key) self.domain = { "name": "Synthetix", "version": "1", "chainId": chain_id, "verifyingContract": "0x0000000000000000000000000000000000000000" } def _sign(self, types, primary_type, message, domain=None): typed_data = { "types": { "EIP712Domain": DOMAIN_FIELDS, **types }, "primaryType": primary_type, "domain": domain or self.domain, "message": message } encoded = encode_structured_data(typed_data) signed = self.account.sign_message(encoded) signature = { "v": signed.v, "r": "0x" + signed.r.hex(), "s": "0x" + signed.s.hex() } validate_signature(signature) return signature def sign_websocket_auth(self, sub_account_id, timestamp=None): if timestamp is None: timestamp = int(time.time()) # Unix seconds return self._sign( types={ "AuthMessage": [ {"name": "subAccountId", "type": "uint256"}, {"name": "timestamp", "type": "uint256"}, {"name": "action", "type": "string"} ] }, primary_type="AuthMessage", message={ "subAccountId": sub_account_id, "timestamp": timestamp, "action": "websocket_auth" } ) def sign_place_order(self, order_data): return self._sign( types={ "Order": [ {"name": "symbol", "type": "string"}, {"name": "side", "type": "string"}, {"name": "orderType", "type": "string"}, {"name": "price", "type": "string"}, {"name": "triggerPrice", "type": "string"}, {"name": "quantity", "type": "string"}, {"name": "reduceOnly", "type": "bool"}, {"name": "isTriggerMarket", "type": "bool"}, {"name": "clientOrderId", "type": "string"}, {"name": "closePosition", "type": "bool"} ], "PlaceOrders": [ {"name": "subAccountId", "type": "uint256"}, {"name": "orders", "type": "Order[]"}, {"name": "grouping", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="PlaceOrders", message={ "subAccountId": order_data['subAccountId'], "orders": order_data['orders'], "grouping": order_data['grouping'], "nonce": order_data.get('nonce', int(time.time() * 1000)), "expiresAfter": order_data.get('expiresAfter', 0) } ) def sign_cancel_order(self, cancel_data): return self._sign( types={ "CancelOrders": [ {"name": "subAccountId", "type": "uint256"}, {"name": "orderIds", "type": "uint256[]"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="CancelOrders", message={ "subAccountId": cancel_data['subAccountId'], "orderIds": cancel_data['orderIds'], "nonce": cancel_data.get('nonce', int(time.time() * 1000)), "expiresAfter": cancel_data.get('expiresAfter', 0) } ) def sign_cancel_all_orders(self, cancel_all_data): return self._sign( types={ "CancelAllOrders": [ {"name": "subAccountId", "type": "uint256"}, {"name": "symbols", "type": "string[]"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="CancelAllOrders", message={ "subAccountId": cancel_all_data['subAccountId'], "symbols": cancel_all_data['symbols'], "nonce": cancel_all_data.get('nonce', int(time.time() * 1000)), "expiresAfter": cancel_all_data.get('expiresAfter', 0) } ) def sign_modify_order(self, modify_data): return self._sign( types={ "ModifyOrder": [ {"name": "subAccountId", "type": "uint256"}, {"name": "orderId", "type": "uint256"}, {"name": "price", "type": "string"}, {"name": "quantity", "type": "string"}, {"name": "triggerPrice", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="ModifyOrder", message={ "subAccountId": modify_data['subAccountId'], "orderId": modify_data['orderId'], "price": modify_data.get('price', ""), "quantity": modify_data.get('quantity', ""), "triggerPrice": modify_data.get('triggerPrice', ""), "nonce": modify_data.get('nonce', int(time.time() * 1000)), "expiresAfter": modify_data.get('expiresAfter', 0) } ) def sign_update_leverage(self, leverage_data): return self._sign( types={ "UpdateLeverage": [ {"name": "subAccountId", "type": "uint256"}, {"name": "symbol", "type": "string"}, {"name": "leverage", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="UpdateLeverage", message={ "subAccountId": leverage_data['subAccountId'], "symbol": leverage_data['symbol'], "leverage": leverage_data['leverage'], "nonce": leverage_data.get('nonce', int(time.time() * 1000)), "expiresAfter": leverage_data.get('expiresAfter', 0) } ) def sign_create_subaccount(self, create_data): return self._sign( types={ "CreateSubaccount": [ {"name": "masterSubAccountId", "type": "uint256"}, {"name": "name", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="CreateSubaccount", message={ "masterSubAccountId": create_data['masterSubAccountId'], "name": create_data['name'], "nonce": create_data.get('nonce', int(time.time() * 1000)), "expiresAfter": create_data.get('expiresAfter', 0) } ) def sign_update_subaccount_name(self, update_data): return self._sign( types={ "UpdateSubAccountName": [ {"name": "subAccountId", "type": "uint256"}, {"name": "name", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="UpdateSubAccountName", message={ "subAccountId": update_data['subAccountId'], "name": update_data['name'], "nonce": update_data.get('nonce', int(time.time() * 1000)), "expiresAfter": update_data.get('expiresAfter', 0) } ) def sign_add_delegated_signer(self, delegate_data): return self._sign( types={ "AddDelegatedSigner": [ {"name": "delegateAddress", "type": "address"}, {"name": "subAccountId", "type": "uint256"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"}, {"name": "expiresAt", "type": "uint256"}, {"name": "permissions", "type": "string[]"} ] }, primary_type="AddDelegatedSigner", message={ "delegateAddress": delegate_data['delegateAddress'], "subAccountId": delegate_data['subAccountId'], "nonce": delegate_data.get('nonce', int(time.time() * 1000)), "expiresAfter": delegate_data.get('expiresAfter', 0), "expiresAt": delegate_data.get('expiresAt', 0), "permissions": delegate_data['permissions'] } ) def sign_remove_all_delegated_signers(self, remove_data): return self._sign( types={ "RemoveAllDelegatedSigners": [ {"name": "subAccountId", "type": "uint256"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="RemoveAllDelegatedSigners", message={ "subAccountId": remove_data['subAccountId'], "nonce": remove_data.get('nonce', int(time.time() * 1000)), "expiresAfter": remove_data.get('expiresAfter', 0) } ) def sign_subaccount_action(self, action_data): return self._sign( types={ "SubAccountAction": [ {"name": "subAccountId", "type": "uint256"}, {"name": "action", "type": "string"}, {"name": "expiresAfter", "type": "uint256"} ] }, primary_type="SubAccountAction", message={ "subAccountId": action_data['subAccountId'], "action": action_data['action'], "expiresAfter": action_data.get('expiresAfter', 0) } ) # Usage Example def example(): signer = SynthetixSigner(private_key="0x...") # WebSocket authentication (timestamp in seconds) auth_sig = signer.sign_websocket_auth(sub_account_id=123456789) print(f"Auth signature: {auth_sig}") # Place orders order_sig = signer.sign_place_order({ "subAccountId": 123456789, "orders": [{ "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000", "triggerPrice": "", "quantity": "0.1", "reduceOnly": False, "isTriggerMarket": False, "clientOrderId": "0x1234567890abcdef1234567890abcdef", "closePosition": False }], "grouping": "na", "nonce": int(time.time() * 1000) }) print(f"Order signature: {order_sig}") # Cancel orders cancel_sig = signer.sign_cancel_order({ "subAccountId": "123456789", "orderIds": ["987654321", "987654322"], "nonce": int(time.time() * 1000) }) print(f"Cancel signature: {cancel_sig}") # Cancel all orders for a symbol cancel_all_sig = signer.sign_cancel_all_orders({ "subAccountId": "123456789", "symbol": "BTC-USDT", "nonce": int(time.time() * 1000) }) print(f"Cancel all signature: {cancel_all_sig}") # Update leverage leverage_sig = signer.sign_update_leverage({ "subAccountId": "123456789", "symbol": "BTC-USDT", "leverage": "20", "nonce": int(time.time() * 1000) }) print(f"Update leverage signature: {leverage_sig}") # Create subaccount create_sig = signer.sign_create_subaccount({ "masterSubAccountId": "123456789", "name": "New Trading Account", "nonce": int(time.time() * 1000) }) print(f"Create subaccount signature: {create_sig}") # Update subaccount name rename_sig = signer.sign_update_subaccount_name({ "subAccountId": "123456789", "name": "Renamed Account", "nonce": int(time.time() * 1000) }) print(f"Update subaccount name signature: {rename_sig}") # Add delegated signer add_delegate_sig = signer.sign_add_delegated_signer({ "delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "subAccountId": "123456789", "permissions": ["session"], "nonce": int(time.time() * 1000) }) print(f"Add delegated signer signature: {add_delegate_sig}") # Remove all delegated signers remove_all_sig = signer.sign_remove_all_delegated_signers({ "subAccountId": "123456789", "nonce": int(time.time() * 1000) }) print(f"Remove all delegated signers signature: {remove_all_sig}") # Get positions (no nonce required for SubAccountAction) positions_sig = signer.sign_subaccount_action({ "subAccountId": "123456789", "action": "getPositions", "expiresAfter": int(time.time() * 1000) + 300000 # Optional: 5 minute expiration (ms) }) print(f"Positions signature: {positions_sig}") ``` ### Go Implementation #### Using go-ethereum ```go package main import ( "crypto/ecdsa" "fmt" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) var domainFields = apitypes.Types{ "EIP712Domain": { {"name", "string"}, {"version", "string"}, {"chainId", "uint256"}, {"verifyingContract", "address"}, }, } type SynthetixSigner struct { privateKey *ecdsa.PrivateKey domain apitypes.TypedDataDomain } func NewSynthetixSigner(privateKeyHex string, chainID int64) (*SynthetixSigner, error) { privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { return nil, err } domain := apitypes.TypedDataDomain{ Name: "Synthetix", Version: "1", ChainId: math.NewHexOrDecimal256(chainID), VerifyingContract: "0x0000000000000000000000000000000000000000", } return &SynthetixSigner{ privateKey: privateKey, domain: domain, }, nil } // SignWebSocketAuth signs authentication for WebSocket connection func (s *SynthetixSigner) SignWebSocketAuth(subAccountId uint64, timestamp int64) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "AuthMessage": { {"subAccountId", "uint256"}, {"timestamp", "uint256"}, {"action", "string"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(subAccountId)), "timestamp": math.NewHexOrDecimal256(timestamp), "action": "websocket_auth", } typedData := apitypes.TypedData{ Types: types, PrimaryType: "AuthMessage", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignPlaceOrder signs a place orders request func (s *SynthetixSigner) SignPlaceOrder(orderData PlaceOrderData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "Order": { {"symbol", "string"}, {"side", "string"}, {"orderType", "string"}, {"price", "string"}, {"triggerPrice", "string"}, {"quantity", "string"}, {"reduceOnly", "bool"}, {"isTriggerMarket", "bool"}, {"clientOrderId", "string"}, {"closePosition", "bool"}, }, "PlaceOrders": { {"subAccountId", "uint256"}, {"orders", "Order[]"}, {"grouping", "string"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(orderData.SubAccountID)), "orders": orderData.Orders, "grouping": orderData.Grouping, "nonce": math.NewHexOrDecimal256(orderData.Nonce), "expiresAfter": math.NewHexOrDecimal256(orderData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "PlaceOrders", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignCancelOrder signs a cancel orders request func (s *SynthetixSigner) SignCancelOrder(cancelData CancelData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "CancelOrders": { {"subAccountId", "uint256"}, {"orderIds", "uint256[]"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(cancelData.SubAccountID)), "orderIds": cancelData.OrderIDs, "nonce": math.NewHexOrDecimal256(cancelData.Nonce), "expiresAfter": math.NewHexOrDecimal256(cancelData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "CancelOrders", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignCancelAllOrders signs a cancel all orders request func (s *SynthetixSigner) SignCancelAllOrders(cancelAllData CancelAllData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "CancelAllOrders": { {"subAccountId", "uint256"}, {"symbols", "string[]"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(cancelAllData.SubAccountID)), "symbols": cancelAllData.Symbols, "nonce": math.NewHexOrDecimal256(cancelAllData.Nonce), "expiresAfter": math.NewHexOrDecimal256(cancelAllData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "CancelAllOrders", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignModifyOrder signs a modify order request func (s *SynthetixSigner) SignModifyOrder(modifyData ModifyData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "ModifyOrder": { {"subAccountId", "uint256"}, {"orderId", "uint256"}, {"price", "string"}, {"quantity", "string"}, {"triggerPrice", "string"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(modifyData.SubAccountID)), "orderId": math.NewHexOrDecimal256(int64(modifyData.OrderID)), "price": modifyData.Price, "quantity": modifyData.Quantity, "triggerPrice": modifyData.TriggerPrice, "nonce": math.NewHexOrDecimal256(modifyData.Nonce), "expiresAfter": math.NewHexOrDecimal256(modifyData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "ModifyOrder", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignUpdateLeverage signs an update leverage request func (s *SynthetixSigner) SignUpdateLeverage(leverageData LeverageData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "UpdateLeverage": { {"subAccountId", "uint256"}, {"symbol", "string"}, {"leverage", "string"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(leverageData.SubAccountID)), "symbol": leverageData.Symbol, "leverage": leverageData.Leverage, "nonce": math.NewHexOrDecimal256(leverageData.Nonce), "expiresAfter": math.NewHexOrDecimal256(leverageData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "UpdateLeverage", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignCreateSubaccount signs a create subaccount request func (s *SynthetixSigner) SignCreateSubaccount(createData CreateSubaccountData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "CreateSubaccount": { {"masterSubAccountId", "uint256"}, {"name", "string"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "masterSubAccountId": math.NewHexOrDecimal256(int64(createData.MasterSubAccountID)), "name": createData.Name, "nonce": math.NewHexOrDecimal256(createData.Nonce), "expiresAfter": math.NewHexOrDecimal256(createData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "CreateSubaccount", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignUpdateSubAccountName signs an update subaccount name request func (s *SynthetixSigner) SignUpdateSubAccountName(updateData UpdateSubAccountNameData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "UpdateSubAccountName": { {"subAccountId", "uint256"}, {"name", "string"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(updateData.SubAccountID)), "name": updateData.Name, "nonce": math.NewHexOrDecimal256(updateData.Nonce), "expiresAfter": math.NewHexOrDecimal256(updateData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "UpdateSubAccountName", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignAddDelegatedSigner signs an add delegated signer request func (s *SynthetixSigner) SignAddDelegatedSigner(delegateData AddDelegatedSignerData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "AddDelegatedSigner": { {"delegateAddress", "address"}, {"subAccountId", "uint256"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, {"expiresAt", "uint256"}, {"permissions", "string[]"}, }, } message := apitypes.TypedDataMessage{ "delegateAddress": delegateData.DelegateAddress, "subAccountId": math.NewHexOrDecimal256(int64(delegateData.SubAccountID)), "nonce": math.NewHexOrDecimal256(delegateData.Nonce), "expiresAfter": math.NewHexOrDecimal256(delegateData.ExpiresAfter), "expiresAt": math.NewHexOrDecimal256(delegateData.ExpiresAt), "permissions": delegateData.Permissions, } typedData := apitypes.TypedData{ Types: types, PrimaryType: "AddDelegatedSigner", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // SignRemoveAllDelegatedSigners signs a remove all delegated signers request func (s *SynthetixSigner) SignRemoveAllDelegatedSigners(removeData RemoveAllDelegatedSignersData) (string, error) { types := apitypes.Types{ "EIP712Domain": domainFields["EIP712Domain"], "RemoveAllDelegatedSigners": { {"subAccountId", "uint256"}, {"nonce", "uint256"}, {"expiresAfter", "uint256"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(int64(removeData.SubAccountID)), "nonce": math.NewHexOrDecimal256(removeData.Nonce), "expiresAfter": math.NewHexOrDecimal256(removeData.ExpiresAfter), } typedData := apitypes.TypedData{ Types: types, PrimaryType: "RemoveAllDelegatedSigners", Domain: s.domain, Message: message, } return s.signTypedData(typedData) } // validateSignature validates EIP-712 signature components func validateSignature(sig []byte) error { if len(sig) != 65 { return fmt.Errorf("invalid signature length: expected 65, got %d", len(sig)) } // Validate v (recovery ID) - must be 0, 1, 27, or 28 v := sig[64] if v != 0 && v != 1 && v != 27 && v != 28 { return fmt.Errorf("invalid v value: %d. Must be 0, 1, 27, or 28", v) } // r and s are validated by length (32 bytes each) r := new(big.Int).SetBytes(sig[:32]) s := new(big.Int).SetBytes(sig[32:64]) if r.Sign() == 0 { return fmt.Errorf("invalid r value: must be non-zero") } if s.Sign() == 0 { return fmt.Errorf("invalid s value: must be non-zero") } return nil } // signTypedData is a helper function to sign typed data and return hex signature func (s *SynthetixSigner) signTypedData(typedData apitypes.TypedData) (string, error) { domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) if err != nil { return "", fmt.Errorf("failed to hash domain: %w", err) } typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) if err != nil { return "", fmt.Errorf("failed to hash message: %w", err) } // Calculate the digest rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) digest := crypto.Keccak256Hash(rawData) // Sign the digest signature, err := crypto.Sign(digest.Bytes(), s.privateKey) if err != nil { return "", fmt.Errorf("failed to sign: %w", err) } // Adjust recovery ID for Ethereum if signature[64] < 27 { signature[64] += 27 } // Validate signature before returning if err := validateSignature(signature); err != nil { return "", fmt.Errorf("invalid signature: %w", err) } return "0x" + common.Bytes2Hex(signature), nil } // Data structures type OrderItem struct { Symbol string `json:"symbol"` Side string `json:"side"` OrderType string `json:"orderType"` Price string `json:"price"` TriggerPrice string `json:"triggerPrice"` Quantity string `json:"quantity"` ReduceOnly bool `json:"reduceOnly"` IsTriggerMarket bool `json:"isTriggerMarket"` ClientOrderId string `json:"clientOrderId"` ClosePosition bool `json:"closePosition"` } type PlaceOrderData struct { SubAccountID uint64 `json:"subAccountId"` Orders []interface{} `json:"orders"` // Typed data message format Grouping string `json:"grouping"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type CancelData struct { SubAccountID uint64 `json:"subAccountId"` OrderIDs []interface{} `json:"orderIds"` // uint256[] as typed data Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type CancelAllData struct { SubAccountID uint64 `json:"subAccountId"` Symbols []string `json:"symbols"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type ModifyData struct { SubAccountID uint64 `json:"subAccountId"` OrderID uint64 `json:"orderId"` Price string `json:"price"` Quantity string `json:"quantity"` TriggerPrice string `json:"triggerPrice"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type LeverageData struct { SubAccountID uint64 `json:"subAccountId"` Symbol string `json:"symbol"` Leverage string `json:"leverage"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type CreateSubaccountData struct { MasterSubAccountID uint64 `json:"masterSubAccountId"` Name string `json:"name"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type UpdateSubAccountNameData struct { SubAccountID uint64 `json:"subAccountId"` Name string `json:"name"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } type AddDelegatedSignerData struct { DelegateAddress string `json:"delegateAddress"` SubAccountID uint64 `json:"subAccountId"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` ExpiresAt int64 `json:"expiresAt"` Permissions []string `json:"permissions"` } type RemoveAllDelegatedSignersData struct { SubAccountID uint64 `json:"subAccountId"` Nonce int64 `json:"nonce"` ExpiresAfter int64 `json:"expiresAfter"` } // Usage example func main() { signer, _ := NewSynthetixSigner("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1) // WebSocket authentication (timestamp in seconds) authSig, _ := signer.SignWebSocketAuth(123456789, time.Now().Unix()) fmt.Printf("Auth signature: %s\n", authSig) // Place order orderData := PlaceOrderData{ SubAccountID: 123456789, Orders: []interface{}{ map[string]interface{}{ "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "isTriggerMarket": false, "clientOrderId": "0x1234567890abcdef1234567890abcdef", "closePosition": false, }, }, Grouping: "na", Nonce: time.Now().UnixMilli(), } orderSig, _ := signer.SignPlaceOrder(orderData) fmt.Printf("Order signature: %s\n", orderSig) // Cancel all orders for specific markets cancelAllData := CancelAllData{ SubAccountID: 123456789, Symbols: []string{"BTC-USDT"}, Nonce: time.Now().UnixMilli(), } cancelAllSig, _ := signer.SignCancelAllOrders(cancelAllData) fmt.Printf("Cancel all signature: %s\n", cancelAllSig) // Update leverage leverageData := LeverageData{ SubAccountID: 123456789, Symbol: "BTC-USDT", Leverage: "20", Nonce: time.Now().UnixMilli(), } leverageSig, _ := signer.SignUpdateLeverage(leverageData) fmt.Printf("Leverage signature: %s\n", leverageSig) // Create subaccount createData := CreateSubaccountData{ MasterSubAccountID: 123456789, // An existing subaccount you own Name: "New Trading Account", Nonce: time.Now().UnixMilli(), } createSig, _ := signer.SignCreateSubaccount(createData) fmt.Printf("Create subaccount signature: %s\n", createSig) // Update subaccount name updateNameData := UpdateSubAccountNameData{ SubAccountID: 123456789, Name: "Renamed Account", Nonce: time.Now().UnixMilli(), } updateNameSig, _ := signer.SignUpdateSubAccountName(updateNameData) fmt.Printf("Update subaccount name signature: %s\n", updateNameSig) // Add delegated signer addDelegateData := AddDelegatedSignerData{ DelegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", SubAccountID: 123456789, Permissions: []string{"session"}, Nonce: time.Now().UnixMilli(), } addDelegateSig, _ := signer.SignAddDelegatedSigner(addDelegateData) fmt.Printf("Add delegated signer signature: %s\n", addDelegateSig) // Remove all delegated signers removeAllData := RemoveAllDelegatedSignersData{ SubAccountID: 123456789, Nonce: time.Now().UnixMilli(), } removeAllSig, _ := signer.SignRemoveAllDelegatedSigners(removeAllData) fmt.Printf("Remove all delegated signers signature: %s\n", removeAllSig) } ``` ### Signature Validation #### Local Validation (JavaScript) ```javascript function validateSignature(signature) { // Check signature structure if (!signature || typeof signature !== 'object') { throw new Error('Signature must be an object'); } const { v, r, s } = signature; // Validate v component (can be 0, 1, 27, or 28) if (typeof v !== 'number' || ![0, 1, 27, 28].includes(v)) { throw new Error('Invalid v component: must be 0, 1, 27, or 28'); } // Validate r component if (typeof r !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(r)) { throw new Error('Invalid r component: must be 32-byte hex string'); } // Validate s component if (typeof s !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(s)) { throw new Error('Invalid s component: must be 32-byte hex string'); } return true; } ``` ### Common Issues and Solutions #### Issue: "Invalid signature" Error **Possible Causes:** 1. Wrong domain separator (must use zero address as verifying contract) 2. Missing `subAccountId` field 3. Incorrect message structure 4. Wrong domain name (must be "Synthetix") 5. Missing or wrong verifyingContract (must be zero address) **Solution:** ```javascript // Verify domain matches exactly const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; // orderType is a plain string — no JSON.stringify needed const order = { symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", // Plain string value price: "50000", // ... other fields }; // Include subAccountId in all trading messages const message = { subAccountId: "123456789", // Required field // ... other fields }; ``` #### Issue: "Missing subAccountId" Error **Cause:** New required field not included **Solution:** ```javascript // SubAccountId required for all trading operations const orderMessage = { subAccountId: BigInt(userSubAccountId), // Required symbol: "BTC-USDT", side: "buy", quantity: "0.1", price: "50000", orderType: "limitGtc", triggerPrice: "", isTriggerMarket: false, reduceOnly: false, nonce: BigInt(Date.now()), expiresAfter: BigInt(0) }; ``` #### Issue: "Nonce already used" Error **Cause:** Replay protection triggered — the nonce value was already used for a previous request. **Solution:** ```javascript // Each nonce must be a positive integer, incrementing and unique per request // Option 1: Use crypto random (recommended - guaranteed unique) const nonce = BigInt('0x' + crypto.randomBytes(8).toString('hex')); // Option 2: Use timestamp (simple, usually unique) const nonce = Date.now(); // For WebSocket auth timestamp field (this IS a timestamp, in seconds) const timestamp = Math.floor(Date.now() / 1000); ``` #### Issue: WebSocket Authentication Failed **Cause:** Wrong domain name or message structure **Solution:** ```javascript // Use correct domain for WebSocket authentication const wsDomain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const authMessage = { subAccountId: BigInt(subAccountId), timestamp: BigInt(Math.floor(Date.now() / 1000)), // Unix timestamp in seconds action: "websocket_auth" // Exact string required }; ``` ### Testing Your Implementation ```javascript // Test all signature types async function testImplementation() { const signer = new SynthetixSigner("0x..." as `0x${string}`); try { // Test WebSocket auth const authSig = await signer.signWebSocketAuth("123456789"); console.log('WebSocket auth signature valid'); // Test order placement const orderSig = await signer.signPlaceOrder({ subAccountId: "123456789", orders: [{ symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", price: "99999", triggerPrice: "", quantity: "0.001", reduceOnly: false, isTriggerMarket: false, clientOrderId: "0x1234567890abcdef1234567890abcdef", closePosition: false }], grouping: "na", nonce: Date.now() }); console.log('Order signature valid'); // Test order cancellation const cancelSig = await signer.signCancelOrder({ subAccountId: "123456789", orderIds: ["999999999"], nonce: Date.now() }); console.log('Cancel signature valid'); // Test order modification const modifySig = await signer.signModifyOrder({ subAccountId: "123456789", orderId: "999999999", price: "99998", quantity: "0.002", nonce: Date.now() }); console.log('Modify signature valid'); } catch (error) { console.error('Signature test failed:', error.message); } } ``` ### Key Differences from Standard EIP-712 1. **Zero Address Contract**: Off-chain authentication uses the zero address (0x0000000000000000000000000000000000000000) as the verifying contract 2. **SubAccount Required**: All trading operations require a `subAccountId` field 3. **Simplified Modify**: Modify orders only allow changing `price`, `quantity`, and/or `triggerPrice` (all optional) 4. **String Types**: Financial values (`price`, `quantity`) are strings for precision 5. **Domain Name**: Unified domain name "Synthetix" for all authentication 6. **Consistent TIF Format**: Time in force values use PascalCase in both EIP-712 signing and API responses (`"Gtc"`, `"Alo"`, `"Ioc"`) ### Important Note on Field Naming **EIP-712 signing and API responses now use consistent format:** * **Both EIP-712 signing and API responses**: `timeInForce: "Gtc"` (PascalCase) Consistent format required across all API operations and signing workflows. ### Next Steps * **[Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management)** - Proper nonce handling strategies * **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Debug authentication issues * **[WebSocket Guide](https://developers.synthetix.io/developer-resources/api/ws-api)** - WebSocket implementation * **[REST API Guide](https://developers.synthetix.io/developer-resources/api/rest-api)** - REST endpoint usage ## Authentication Examples For comprehensive implementation examples across all languages and frameworks, see **[EIP-712 Signing Implementation](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)**. ### Quick Integration Examples Minimal integration examples. See EIP-712 guide for complete implementation details. #### JavaScript/TypeScript with viem ```typescript // Minimal WebSocket auth example import { createWalletClient, http, parseSignature } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount('0x...'); const client = createWalletClient({ account, transport: http() }); // Sign WebSocket authentication const authSignature = await client.signTypedData({ domain: { name: "Synthetix", version: "1", chainId: BigInt(1) }, types: { AuthMessage: [ { name: "subAccountId", type: "uint256" }, { name: "timestamp", type: "uint256" }, { name: "action", type: "string" } ] }, primaryType: "AuthMessage", message: { subAccountId: BigInt("123456789"), timestamp: BigInt(Math.floor(Date.now() / 1000)), action: "websocket_auth" } }); const { v, r, s } = parseSignature(authSignature); ``` #### Python with eth\_account ```python from eth_account.messages import encode_structured_data from eth_account import Account import time # Minimal order signing example account = Account.from_key('0x...') typed_data = { "types": { "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"} ], "Order": [ {"name": "symbol", "type": "string"}, {"name": "side", "type": "string"}, {"name": "quantity", "type": "string"}, {"name": "orderType", "type": "string"}, {"name": "price", "type": "string"}, {"name": "triggerPrice", "type": "string"}, {"name": "reduceOnly", "type": "bool"}, {"name": "isTriggerMarket", "type": "bool"}, {"name": "clientOrderId", "type": "string"}, {"name": "closePosition", "type": "bool"} ], "PlaceOrders": [ {"name": "subAccountId", "type": "uint256"}, {"name": "orders", "type": "Order[]"}, {"name": "grouping", "type": "string"}, {"name": "nonce", "type": "uint256"}, {"name": "expiresAfter", "type": "uint256"} ] }, "primaryType": "PlaceOrders", "domain": { "name": "Synthetix", "version": "1", "chainId": 1 }, "message": { "subAccountId": 123456789, "orders": [{ "symbol": "BTC-USDT", "side": "buy", "quantity": "0.1", "orderType": "limitGtc", "price": "50000", "triggerPrice": "", "reduceOnly": False, "isTriggerMarket": False, "clientOrderId": "", "closePosition": False }], "grouping": "", "nonce": int(time.time() * 1000), "expiresAfter": 0 } } encoded = encode_structured_data(typed_data) signed = account.sign_message(encoded) ``` #### Go with go-ethereum ```go // Minimal auth example import ( "time" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) domain := apitypes.TypedDataDomain{ Name: "Synthetix", Version: "1", ChainId: math.NewHexOrDecimal256(1), } types := apitypes.Types{ "EIP712Domain": { {"name", "string"}, {"version", "string"}, {"chainId", "uint256"}, }, "AuthMessage": { {"subAccountId", "uint256"}, {"timestamp", "uint256"}, {"action", "string"}, }, } message := apitypes.TypedDataMessage{ "subAccountId": math.NewHexOrDecimal256(123456789), "timestamp": math.NewHexOrDecimal256(time.Now().Unix()), "action": "websocket_auth", } typedData := apitypes.TypedData{ Types: types, PrimaryType: "AuthMessage", Domain: domain, Message: message, } // Sign with your private key... ``` ### Integration Patterns #### React Hook Pattern ```typescript // Custom hook for Synthetix signing export const useSynthetixSigner = (privateKey: string) => { const signer = useMemo(() => new SynthetixSigner(privateKey), [privateKey]); const signOrder = useCallback(async (orderData) => { return await signer.signPlaceOrder(orderData); }, [signer]); const signAuth = useCallback(async (subAccountId: string) => { return await signer.signWebSocketAuth(subAccountId); }, [signer]); return { signOrder, signAuth }; }; ``` #### Error Handling Pattern ```typescript try { const signature = await signer.signPlaceOrder(orderData); // Use signature in API call } catch (error) { if (error.message.includes('subAccountId')) { console.error('Missing required subAccountId field'); } else if (error.message.includes('orderType')) { console.error('Order type must be JSON stringified'); } else { console.error('Signature failed:', error.message); } } ``` ### For Complete Examples * **[EIP-712 Signing Implementation](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - Full implementations in all languages * **[Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management)** - Proper nonce handling strategies * **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Debug authentication issues ## Authentication All trading endpoints on the Synthetix API require authentication using EIP-712 signatures. This section provides comprehensive guidance on implementing secure authentication for both REST and WebSocket APIs. ### Quick Start 1. **[Overview](https://developers.synthetix.io/developer-resources/api/authentication/overview)** - Understanding the authentication system 2. **[EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - Cryptographic signature implementation 3. **[Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management)** - Preventing replay attacks 4. **[Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples)** - Ready-to-use code samples 5. **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Debug common authentication issues ### Authentication Flow ``` 1. Generate Nonce ↓ 2. Create Message ↓ 3. Sign with EIP-712 ↓ 4. Send Request ↓ 5. API Validates Signature ↓ 6. Execute Operation ``` ### Key Concepts #### EIP-712 Signatures * **Type Safety**: Structured data signing prevents errors * **Human Readable**: Clear display of what you're signing * **Hardware Wallet Support**: Compatible with Ledger, Trezor, etc. * **Phishing Protection**: Domain separation prevents attack vectors #### Nonce Management * **Replay Protection**: Each request uses a unique, incrementing nonce * **Format**: Nonce values are positive integers * **Order Enforcement**: Nonces must be strictly increasing per account #### Subaccount Support * **Multiple Accounts**: Trade across multiple subaccounts under one wallet * **Secure Access**: Signature determines authorization for subaccount operations ### Authentication Requirements | Endpoint Type | Authentication Required | | ------------------- | ------------------------------------------ | | **Info Endpoints** | No - Public market data | | **Trade Endpoints** | Yes - EIP-712 signature required | | **WebSocket Info** | No - Public subscriptions | | **WebSocket Trade** | Yes - Initial auth + per-action signatures | ### Authentication Flow Details #### REST API Each request to the Trade endpoint requires its own EIP-712 signature containing: * Request data (orders, cancellations, modifications) * Subaccount ID * Nonce (positive integer for replay protection) * Optional expiration time #### WebSocket API Two-step authentication process: 1. **Initial Authentication**: Sign WebSocket auth message with `subAccountId`, `timestamp`, and `action: "websocket_auth"` 2. **Per-Action Signatures**: Each trading action still requires individual EIP-712 signatures ### Error Handling Common authentication errors and solutions: | Error | Cause | Solution | | -------------------------------- | ------------------------------ | ------------------------------------------- | | `Invalid signature` | Wrong signing parameters | Verify domain, types, and message structure | | `Nonce already used` | Replay attack protection | Use a fresh, incrementing nonce | | `Request expired` | Clock drift or slow request | Increase expiration buffer | | `Unauthorized subaccount access` | Missing delegation permissions | Verify delegation for subaccount | ### Next Steps * **New to EIP-712?** Start with [Overview](https://developers.synthetix.io/developer-resources/api/authentication/overview) * **Ready to implement?** Jump to [Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples) * **Having issues?** Check [Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting) * **Need WebSocket auth?** See [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) ### Related Resources * [API General Information](https://developers.synthetix.io/developer-resources/api/general-information) - API basics and conventions * [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) - Request rate limiting * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Comprehensive error reference * [Notation](https://developers.synthetix.io/developer-resources/api/notation) - Data format conventions ## Nonce Management The nonce system is a critical security feature that prevents replay attacks and ensures request uniqueness in the Synthetix APIs. Every authenticated request must include an incrementing nonce. ### Overview A nonce (number used once) serves two purposes: 1. **Replay Protection**: Prevents malicious actors from re-submitting your signed requests 2. **Request Ordering**: Ensures requests are processed in the intended order ### Nonce Requirements #### Format * Must be a positive integer * Recommended: Use an incrementing sequence (for example `Date.now()` with conflict handling) * Must be strictly increasing (each nonce > previous nonce) * Must be unique (must not be reused for the same signer/subaccount context) * Maximum value: `9223372036854775807` (max safe integer) #### Example ```json { "action": { "action": "placeOrders", "order": { ... } }, "nonce": 1704067200000, // Positive integer nonce "signature": { ... } } ``` ### Implementation Notes #### 1. Use Timestamp as a Convenience Nonce Source One convenient approach is using the current timestamp: ```javascript function generateNonce() { return Date.now(); // Returns a positive integer nonce value } // Example: 1704067200000 ``` ```python import time def generate_nonce(): return int(time.time() * 1000) # Returns a positive integer nonce value # Example: 1704067200000 ``` #### 2. Handle Nonce Conflicts If multiple requests are sent with the same base value, increment the nonce: ```javascript class NonceManager { constructor() { this.lastNonce = 0; } generateNonce() { const timestamp = Date.now(); // Ensure nonce is always increasing this.lastNonce = Math.max(timestamp, this.lastNonce + 1); return this.lastNonce; } } const nonceManager = new NonceManager(); const nonce1 = nonceManager.generateNonce(); // 1704067200000 const nonce2 = nonceManager.generateNonce(); // 1704067200001 (incremented) ``` #### 3. Synchronized Nonce Across Systems When running multiple trading systems with the same account: ```javascript class DistributedNonceManager { constructor(systemId, totalSystems) { this.systemId = systemId; // 0, 1, 2, etc. this.totalSystems = totalSystems; this.counter = 0; } generateNonce() { // Each system uses a different base value const baseTime = Date.now(); const offset = this.systemId; // Ensure uniqueness across systems return baseTime * this.totalSystems + offset; } } // System 1: nonces end in 0: 17040672000000, 17040672000010, ... // System 2: nonces end in 1: 17040672000001, 17040672000011, ... ``` ### Error Handling #### Nonce Already Used Error When a nonce has already been used: ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "Nonce already used", "details": { "lastNonce": 1704067200000, "attemptedNonce": 1704067199000 } }, "request_id": "5ccf215d37e3ae6d", "timestamp": "2025-01-01T00:00:00Z" } ``` #### Recovery Strategy ```javascript async function executeWithNonceRetry(action, maxRetries = 3) { const nonceManager = new NonceManager(); for (let i = 0; i < maxRetries; i++) { try { const nonce = nonceManager.generateNonce(); const response = await sendRequest({ action, nonce, signature: signRequest(action, nonce) }); return response; } catch (error) { if (error.message.includes('Nonce already used') && i < maxRetries - 1) { // Force increment for next attempt nonceManager.lastNonce += 1000; continue; } throw error; } } } ``` ### Advanced Nonce Strategies #### 1. High-Frequency Trading For HFT systems sending hundreds of requests per second: ```javascript class HFTNonceManager { constructor() { this.baseTime = Date.now(); this.counter = 0; } generateNonce() { // Combine timestamp with microsecond counter const elapsed = Date.now() - this.baseTime; this.counter++; // Format: [timestamp][counter] // Ensures uniqueness even within same millisecond return parseInt(`${Date.now()}${String(this.counter).padStart(6, '0')}`); } reset() { // Reset counter periodically to prevent overflow if (this.counter > 999999) { this.baseTime = Date.now(); this.counter = 0; } } } ``` #### 2. Batch Operations When sending batch operations, use sequential nonces: ```javascript function generateBatchNonces(batchSize) { const baseNonce = Date.now(); const nonces = []; for (let i = 0; i < batchSize; i++) { nonces.push(baseNonce + i); } return nonces; } // Example: Placing 3 orders in parallel const nonces = generateBatchNonces(3); // [1704067200000, 1704067200001, 1704067200002] ``` #### 3. Fallback Nonce Sources Multiple nonce generation strategies for reliability: ```javascript class RobustNonceManager { constructor() { this.lastNonce = 0; } generateNonce() { const strategies = [ () => Date.now(), () => Date.now() * 1000 + Math.floor(Math.random() * 1000), () => this.lastNonce + 1000, () => BigInt(Date.now()) * 1000000n + BigInt(process.hrtime()[1]) ]; for (const strategy of strategies) { try { const nonce = Number(strategy()); if (nonce > this.lastNonce) { this.lastNonce = nonce; return nonce; } } catch (e) { continue; } } // Ultimate fallback this.lastNonce += 1; return this.lastNonce; } } ``` ### Monitoring and Debugging #### Nonce Tracking Keep track of nonces for debugging: ```javascript class DebugNonceManager { constructor() { this.lastNonce = 0; this.nonceHistory = []; } generateNonce() { const nonce = Date.now(); this.lastNonce = Math.max(nonce, this.lastNonce + 1); // Track for debugging this.nonceHistory.push({ nonce: this.lastNonce, timestamp: new Date().toISOString(), stack: new Error().stack }); // Keep only last 100 for memory efficiency if (this.nonceHistory.length > 100) { this.nonceHistory.shift(); } return this.lastNonce; } getHistory() { return this.nonceHistory; } } ``` #### Common Issues and Solutions | Issue | Cause | Solution | | ----------------------- | ------------------ | --------------------------------- | | "Nonce already used" | System clock drift | Sync system time with NTP | | "Nonce must be greater" | Nonce decreased | Ensure nonce is always increasing | | Frequent conflicts | Multiple systems | Use distributed nonce strategy | | Integer overflow | Nonce too large | Reset or use modulo arithmetic | ### Security * **Never Reuse Nonces**: Each nonce must be unique and increasing per account * **Don't Use Predictable Patterns**: While timestamps are recommended, add randomness for sensitive operations * **Validate Locally**: Check nonce increment before sending to avoid unnecessary API calls * **Secure Storage**: If persisting last nonce, store securely to prevent tampering ### Implementation Examples #### Full TypeScript Example ```typescript interface NonceManager { generateNonce(): bigint; } class ProductionNonceManager implements NonceManager { private lastNonce: bigint = 0n; private readonly maxNonce = BigInt(Number.MAX_SAFE_INTEGER); generateNonce(): bigint { const timestamp = BigInt(Date.now()); // Ensure always increasing this.lastNonce = timestamp > this.lastNonce ? timestamp : this.lastNonce + 1n; // Check bounds if (this.lastNonce > this.maxNonce) { throw new Error('Nonce overflow - reset required'); } return this.lastNonce; } reset(): void { this.lastNonce = 0n; } } ``` #### Python Example ```python import time import threading from typing import Optional class NonceManager: def __init__(self): self.last_nonce: int = 0 self.lock = threading.Lock() def generate_nonce(self) -> int: with self.lock: timestamp = int(time.time() * 1000) self.last_nonce = max(timestamp, self.last_nonce + 1) return self.last_nonce def reset(self) -> None: with self.lock: self.last_nonce = 0 # Global instance for thread safety nonce_manager = NonceManager() ``` ### Testing Nonce Management Always test your nonce management system: ```javascript // Test uniqueness const manager = new NonceManager(); const nonces = Array(1000).fill(0).map(() => manager.generateNonce()); const uniqueNonces = new Set(nonces); console.assert(uniqueNonces.size === nonces.length, 'All nonces must be unique'); // Test incrementing const isIncrementing = nonces.every((n, i) => i === 0 || n > nonces[i - 1]); console.assert(isIncrementing, 'Nonces must be incrementing'); ``` ### Integration with EIP-712 Signing When using nonces with EIP-712 signatures: ```javascript import { SynthetixSigner } from './eip-712-signing'; class AuthenticatedRequestManager { constructor(privateKey) { this.signer = new SynthetixSigner(privateKey); this.nonceManager = new NonceManager(); } async placeOrders(orders, expiresAfter) { const nonce = this.nonceManager.generateNonce(); const signature = await this.signer.signPlaceOrders(orders, nonce, expiresAfter); return { action: { type: "placeOrders", orders }, nonce, signature, expiresAfter }; } } ``` ### Next Steps * **[Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples)** - Complete working implementations with nonce management * **[EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - How nonces integrate with signatures * **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Debug nonce-related issues ### Related Documentation * [Authentication Overview](https://developers.synthetix.io/developer-resources/api/authentication/overview) - General authentication concepts * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Handle nonce errors properly ## Authentication Overview The Synthetix API uses **EIP-712 signatures** for secure, cryptographic authentication. This approach ensures that only authorized accounts can execute trades while maintaining the highest security standards. ### Why EIP-712? EIP-712 (Ethereum Improvement Proposal 712) is the gold standard for signing structured data in the Ethereum ecosystem: #### Security * **Type Safety**: Prevents signing malformed or malicious data * **Domain Separation**: Protects against cross-contract signature replay attacks * **Human Readable**: Users can see exactly what they're signing ### Authentication Components Every authenticated request consists of three key components: #### Action Object Contains the specific operation you want to perform: ```json { "action": "placeOrders", "orders": [{ "symbol": "BTC-USDT", "side": "buy", "quantity": "0.1", "price": "50000", "orderType": "limitGtc", "triggerPrice": "", "isTriggerMarket": false, "reduceOnly": false }] } ``` #### Nonce A unique, incrementing number to prevent replay attacks: ```json { "nonce": 1704067200000 // Positive integer nonce } ``` #### EIP-712 Signature Cryptographic proof that you authorized this specific request: ```json { "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ##### Signature Format Validation All EIP-712 signatures must conform to the following strict format: | Component | Type | Valid Values | Description | | --------- | ------- | --------------- | -------------------------------- | | `v` | integer | 0, 1, 27, or 28 | Recovery ID (typically 27 or 28) | | `r` | string | 32 bytes hex | First 32 bytes of the signature | | `s` | string | 32 bytes hex | Second 32 bytes of the signature | **Validation Rules:** * `v` must be exactly one of: 0, 1, 27, or 28 * `r` must match regex: `^0x[a-fA-F0-9]{64}$` (0x prefix + 64 hex chars) * `s` must match regex: `^0x[a-fA-F0-9]{64}$` (0x prefix + 64 hex chars) * Both `r` and `s` must be non-zero values ### Authentication Flow ``` CLIENT API BLOCKCHAIN | | | |-- 1. Generate nonce ---| | | | | |-- 2. Create typed ------| | | message | | | | | |-- 3. Sign with --------| | | private key | | | | | |-- 4. Send signed ----->| | | request | | | |-- 5. Validate ---------| | | signature format | | | | | |-- 6. Recover signer -->| | | address | | | | | |-- 7. Check permissions | | | | |<-- 8. Execute ---------| | operation | | ``` #### Step-by-Step Process 1. **Prepare the Request**: Create your action object with the operation details 2. **Generate Nonce**: Use a positive integer, incrementing from the last nonce 3. **Structure the Message**: Combine action, nonce, and optional parameters 4. **Sign with EIP-712**: Use your private key to create the cryptographic signature 5. **Send to API**: Submit the complete authenticated request 6. **API Validation**: Server verifies signature and executes operation ### Domain Separator The domain separator uniquely identifies the Synthetix API and prevents signature reuse across different applications. For the complete domain configuration, see [Environments](https://developers.synthetix.io/environments). **Important**: The domain must match exactly for signatures to be valid. ### Message Types Each API operation has a specific typed data structure. For example, placing orders: ```javascript const types = { PlaceOrders: [ { name: "orders", type: "Order[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ], Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "quantity", type: "string" }, { name: "price", type: "string" }, { name: "orderType", type: "string" }, { name: "reduceOnly", type: "bool" } ] }; ``` ### Optional Parameters #### Request Expiration Add an expiration timestamp to prevent stale request execution: ```json { "expiresAfter": 1704067300 // 5 minutes from nonce (Unix seconds) } ``` #### Subaccount Trading Trade on behalf of another account (requires proper delegation): ```json { "subAccountId": "1867542890123456789" } ``` ### Common Issues * Reusing nonces (breaks replay protection) * Wrong domain separator (signatures will fail) * Exposing private keys in logs or client code * Not handling clock drift (use buffer for expiration) * Signing malformed or unvalidated data ### Important Field Naming Note **HTTP Request Body vs EIP-712 Message:** The HTTP request body uses `subaccountId` (lowercase 'a'), while the EIP-712 message structure uses `subAccountId` (camelCase): ```typescript // HTTP Request Body { "subaccountId": "1867542890123456789", // lowercase 'a' "params": { "action": "getPositions" }, "nonce": 1704067200000, "signature": { /* ... */ } } // EIP-712 Message (what you sign) { "subAccountId": 1867542890123456789, // camelCase 'A' "action": "getPositions", "nonce": 1704067200000, "expiresAfter": 1704070800 } ``` This inconsistency exists in the implementation and must be followed exactly for signatures to verify correctly. ### Next Steps * **[EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - Implementation details and code * **[Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management)** - Handling nonces properly * **[Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples)** - Complete working examples * **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Common issues and solutions ### Related Documentation * [General Information](https://developers.synthetix.io/developer-resources/api/general-information) - API basics * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Real-time trading auth * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Authentication error reference ## Authentication Troubleshooting This guide helps you diagnose and resolve common authentication issues when using the Synthetix API. ### Quick Diagnosis #### Authentication Checklist Before diving into specific errors, verify these basics: * [ ] **Private key is valid** and has proper format * [ ] **Domain separator** matches exactly * [ ] **Nonce is increasing** and hasn't been used * [ ] **Signature components** are properly formatted * [ ] **Network connectivity** to the API endpoints * [ ] **System time is synchronized** (for nonce generation) ### Common Error Messages #### 1. "Invalid signature" **Symptoms:** ```json { "status": "error", "error": { "code": "UNAUTHORIZED", "message": "Invalid signature", "category": "AUTH", "retryable": false, "details": {} }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` **Possible Causes:** ##### Wrong Domain Separator ```javascript // Wrong const domain = { name: "SynthetixOffchain", // Wrong name version: "1", chainId: 1, verifyingContract: "0x1234567890123456789012345678901234567890" // Wrong address }; // Correct const domain = { name: "Synthetix", // Exact match required version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" // Zero address for off-chain }; ``` ##### Incorrect Message Structure ```javascript // Wrong - order type as object const message = { orders: [{ symbol: "BTC-USDT", side: "buy", quantity: "0.1", price: "50000", orderType: "limitGtc", // String enum format triggerPrice: "", isTriggerMarket: false, reduceOnly: false }], nonce, expiresAfter }; // Correct - order type as string const message = { orders: [{ symbol: "BTC-USDT", side: "buy", quantity: "0.1", price: "50000", orderType: "limitGtc", // Direct string enum triggerPrice: "", isTriggerMarket: false, reduceOnly: false }], nonce, expiresAfter }; ``` ##### Chain ID Mismatch ```javascript // Check your target network const chainIds = { mainnet: 1, }; // Domain chainId must match your target const domain = { name: "Synthetix", version: "1", chainId: 1, // Must match your environment verifyingContract: "0x0000000000000000000000000000000000000000" }; ``` **Debug Steps:** 1. **Verify Domain:** ```javascript console.log('Domain used:', JSON.stringify(domain, null, 2)); ``` 2. **Check Message Structure:** ```javascript console.log('Message structure:', JSON.stringify(message, null, 2)); ``` 3. **Validate Signature Components:** ```javascript function validateSignature(sig) { console.log('Signature v:', sig.v, '(should be 27 or 28)'); console.log('Signature r length:', sig.r.length, '(should be 66)'); console.log('Signature s length:', sig.s.length, '(should be 66)'); console.log('Signature r format:', /^0x[0-9a-fA-F]{64}$/.test(sig.r)); console.log('Signature s format:', /^0x[0-9a-fA-F]{64}$/.test(sig.s)); } ``` 4. **Test Signature Recovery:** ```javascript import { ethers } from 'ethers'; // Verify signature locally const typedData = { domain, types, message }; const digest = ethers.utils._TypedDataEncoder.hash(domain, types, message); const recoveredAddress = ethers.utils.recoverAddress(digest, signature); console.log('Expected address:', wallet.address); console.log('Recovered address:', recoveredAddress); console.log('Match:', recoveredAddress.toLowerCase() === wallet.address.toLowerCase()); ``` #### 2. "Nonce already used" **Symptoms:** ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "Nonce already used", "category": "REQUEST", "retryable": false, "details": { "lastNonce": 1704067200000, "attemptedNonce": 1704067199000 } }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` **Causes & Solutions:** ##### Concurrent Requests ```javascript // Use proper nonce management for concurrent requests class ThreadSafeNonceManager { constructor() { this.lastNonce = 0; this.lock = false; } async generateNonce() { // Simple lock mechanism while (this.lock) { await new Promise(resolve => setTimeout(resolve, 10)); } this.lock = true; const timestamp = Date.now(); this.lastNonce = Math.max(timestamp, this.lastNonce + 1); const nonce = this.lastNonce; this.lock = false; return nonce; } } ``` ##### Multiple Systems with Same Key ```javascript // Use distributed nonce strategy class DistributedNonceManager { constructor(systemId, totalSystems) { this.systemId = systemId; this.totalSystems = totalSystems; } generateNonce() { const baseTime = Date.now(); // Each system gets different remainder when divided by total systems return baseTime * this.totalSystems + this.systemId; } } // System 0: nonces end in 0, 3, 6, 9... // System 1: nonces end in 1, 4, 7... // System 2: nonces end in 2, 5, 8... ``` #### 3. "Insufficient margin" **Symptoms:** ```json { "status": "error", "error": { "code": "INSUFFICIENT_MARGIN", "message": "insufficient margin: additional needed 4000.00, available 1000.00", "category": "TRADING", "retryable": false, "details": { "additionalNeeded": "4000.00", "availableMargin": "1000.00" } }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` **Solutions:** ##### Check Account Balance First ```javascript async function checkMarginBeforeOrder(client, order) { // Get account info (implement based on your client) const accountInfo = await client.getAccountInfo(); const availableMargin = parseFloat(accountInfo.availableMargin); // Estimate required margin for order const orderValue = parseFloat(order.price) * parseFloat(order.quantity); const estimatedMargin = orderValue * 0.1; // Rough 10x leverage estimate if (availableMargin < estimatedMargin) { throw new Error(`Insufficient margin: need ${estimatedMargin}, have ${availableMargin}`); } return true; } ``` #### Self-Help Resources * **[Authentication Overview](https://developers.synthetix.io/developer-resources/api/authentication/overview)** - Understand the basics * **[EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - Implementation details * **[Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples)** - Working code samples * **[Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling)** - Comprehensive error reference #### Community Support * **Discord**: Join the Synthetix developer community ## Get Exchange Status Returns the current operational status of the exchange. This endpoint is available even when the exchange is under maintenance and other public endpoints are returning HTTP 503 — making it suitable for polling-based health checks or pre-flight status gates. ### Endpoint | Surface | Method | URL | | ----------------- | ------ | ------------------------------------------------- | | REST service | `GET` | `https://papi.synthetix.io/v1/exchange/status` | | WebSocket service | `GET` | `https://papi.synthetix.io/v1/ws/exchange/status` | Both variants are plain HTTP GET requests — the WebSocket service URL does **not** require or accept a WebSocket upgrade for this path. #### No Authentication Required This is a public endpoint that does not require authentication. ### Request No request body or parameters. #### cURL ```bash curl https://papi.synthetix.io/v1/exchange/status ``` ```bash # Via the WebSocket service URL (plain HTTP, no upgrade) curl https://papi.synthetix.io/v1/ws/exchange/status ``` #### JavaScript ```javascript const response = await fetch('https://papi.synthetix.io/v1/exchange/status'); const data = await response.json(); console.log(data); ``` #### Python ```python import requests response = requests.get('https://papi.synthetix.io/v1/exchange/status') data = response.json() print(data) ``` ### Response #### Success Response ```json { "status": "ok" } ``` #### Response Fields | Field | Type | Description | | -------- | ------ | --------------------------------------------------------------------------- | | `status` | string | Exchange operational status. `"ok"` when the exchange is operating normally | ### Implementation Notes * This endpoint remains accessible when other public endpoints return HTTP 503 during maintenance windows * Poll this endpoint to determine when the exchange returns to normal operation after a maintenance window * Both the REST service URL and the WebSocket service URL serve the same response — use whichever base URL your client already connects to ### Error Handling | HTTP Status | Description | | ----------- | ------------------------------------------------------------------------------------ | | `200` | Exchange is operational | | `503` | Exchange is under maintenance (endpoint itself may also return 503 in extreme cases) | ### Use Cases #### Pre-flight Status Check ```javascript async function waitForExchange(pollIntervalMs = 5000) { while (true) { const response = await fetch('https://papi.synthetix.io/v1/exchange/status'); const data = await response.json(); if (response.ok && data.status === 'ok') return; await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } } ``` #### Health Monitor ```javascript async function isExchangeOperational() { try { const response = await fetch('https://papi.synthetix.io/v1/exchange/status'); return response.ok; } catch { return false; } } ``` ## REST API The Synthetix REST API provides secure, authenticated access to trading operations and public market data through standard HTTP POST requests. ### Overview | Endpoint | Path | Authentication | Purpose | | --------- | ----------- | ----------------- | -------------------------------------- | | **Trade** | `/v1/trade` | EIP-712 signature | Trading operations, account management | | **Info** | `/v1/info` | None | Public market data | All REST endpoints use **POST** method exclusively and accept/return **JSON** data. For connection URLs, see [Environments](https://developers.synthetix.io/environments). ### Authentication Trade endpoints require EIP-712 signature authentication: ```json { "action": { "action": "methodName", // method-specific parameters }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` See [Authentication](https://developers.synthetix.io/developer-resources/api/rest-api/trade/authentication) for detailed signing instructions. ### Request Format All requests must: 1. Use `POST` method 2. Include `Content-Type: application/json` header 3. Send JSON body with required parameters #### Example Request ```bash curl -X POST https://papi.synthetix.io/v1/trade \ -H "Content-Type: application/json" \ -d '{ "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [{ "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "isTriggerMarket": false }] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x...", "s": "0x..." } }' ``` ### Response All responses follow a consistent structure: #### Success Response ```json { "status": "ok", "response": { // Method-specific response data }, "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "code": "ERROR_CODE", "message": "Error description", "details": { /* optional context */ } }, "request_id": "5ccf215d37e3ae6d", "timestamp": "2025-01-01T00:00:00Z" } ``` ### Rate Limits REST API requests are rate-limited: * Trade endpoint: 300 requests per minute per IP * Info endpoint: 600 requests per minute per IP * See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for details ### Usage Examples #### Batch Operations ```json { "action": { "action": "placeOrders", "orders": [ { /* order 1 */ }, { /* order 2 */ }, { /* order 3 */ } ] } } ``` #### Error Handling ```javascript const response = await fetch('https://papi.synthetix.io/v1/trade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); const data = await response.json(); if (data.status === 'error') { console.error('API Error:', data.error); } else { console.log('Success:', data.response); } ``` #### Retry Logic ```javascript async function requestWithRetry(endpoint, payload, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await makeRequest(endpoint, payload); return response; } catch (error) { if (i === maxRetries - 1) throw error; const delay = Math.pow(2, i) * 1000; await sleep(delay); } } } ``` ### WebSocket Alternative For real-time data and lower latency trading, consider using the [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) which offers: * Real-time market data streams * Lower latency for order placement * Reduced overhead for frequent operations ### Next Steps * [Authentication Guide](https://developers.synthetix.io/developer-resources/api/rest-api/trade/authentication) - Set up request signing * [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - Start trading * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Handle errors properly * [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) - Understand rate limiting ## WebSocket API The Synthetix API provides real-time, bidirectional communication for both trading operations and market data streaming. ### Overview The WebSocket API offers the following endpoints: | Endpoint | Path | Authentication | Purpose | | ------------------- | -------------- | -------------- | ------------------------------------------------------------ | | **Trade WebSocket** | `/v1/ws/trade` | Required | Real-time trading operations and authenticated subscriptions | | **Info WebSocket** | `/v1/ws/info` | None | Public market data streams and subscriptions | **Subscriptions**: Both endpoints support subscriptions. Use Trade WebSocket for authenticated subscriptions (account updates, positions, orders) and Info WebSocket for public subscriptions (orderbook, market data). For connection URLs, see [Environments](https://developers.synthetix.io/environments). ### Features * Lower latency through persistent connections * Real-time order status updates and market data * Reduced overhead with no HTTP headers per message * Bidirectional communication with server-pushed updates * Single connection for multiple operations ### Connection Management #### Establishing Connection ```javascript // Trade WebSocket - authenticated trading and account subscriptions const tradeWs = new WebSocket('wss://papi.synthetix.io/v1/ws/trade'); tradeWs.onopen = () => { console.log('Trade WebSocket connected'); // Send authentication message }; tradeWs.onmessage = (event) => { const message = JSON.parse(event.data); // Handle trading responses and account updates }; tradeWs.onclose = (event) => { console.log('Trade WebSocket disconnected:', event.code, event.reason); // Implement reconnection logic }; tradeWs.onerror = (error) => { console.error('Trade WebSocket error:', error); }; ``` ```javascript // Info WebSocket - public market data and subscriptions const infoWs = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); infoWs.onopen = () => { console.log('Info WebSocket connected'); // Subscribe to public market data }; infoWs.onmessage = (event) => { const message = JSON.parse(event.data); // Handle market data updates }; infoWs.onclose = (event) => { console.log('Info WebSocket disconnected:', event.code, event.reason); // Implement reconnection logic }; infoWs.onerror = (error) => { console.error('Info WebSocket error:', error); }; ``` #### Message Format All WebSocket messages use JSON format: ##### Client to Server ```json { "id": "unique-request-id", "method": "post", "params": { "action": "placeOrders", "nonce": 1704067200000, "signature": { "v": 27, "r": "0x...", "s": "0x..." }, "subAccountId": "123456789", // Method-specific parameters follow "orders": [{ /* order details */ }] } } ``` ##### Server to Client (Response) ```json { "id": "unique-request-id", "requestId": "unique-request-id", "status": 200, "timestamp": 1704067200000, "traceId": "abc123def456", "result": { // Method-specific response data } } ``` ##### Server to Client (Error) ```json { "id": "unique-request-id", "requestId": "unique-request-id", "status": 400, "timestamp": 1704067200000, "traceId": "abc123def456", "error": { "errorCode": "VALIDATION_ERROR", "code": 400, "message": "Error description", "category": "REQUEST", "retryable": false, "details": {} } } ``` Error response fields: * `errorCode` - Specific error code for programmatic handling * `category` - Error classification (REQUEST, AUTH, RATE\_LIMIT, TRADING, SYSTEM) * `retryable` - Whether the operation can be retried * `details` - Additional context about the error ##### Server to Client (Push Notification) ```json { "method": "notificationType", "timestamp": 1704067200000, "traceId": "abc123def456", "data": { // Event-specific data } } ``` ### Authentication The Trade WebSocket requires authentication using EIP-712 signatures immediately after connection: ```json { "id": "auth-1", "method": "auth", "params": { "message": "{\"types\":{...},\"primaryType\":\"AuthMessage\",\"domain\":{...},\"message\":{\"subAccountId\":\"12345\",\"timestamp\":1640995200,\"action\":\"websocket_auth\"}}", "signature": "0x1234567890abcdef..." } } ``` **Key Points:** * Authentication must be sent within 30 seconds of connection * Uses EIP-712 typed data signing with `AuthMessage` type * Timestamp must be within ±60 seconds of server time * Once authenticated, the connection remains authenticated until disconnected For complete authentication details, EIP-712 structure, and implementation examples, see [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication). ### Heartbeat & Keep-Alive WebSocket connections require periodic heartbeats: * **Ping Interval**: Send ping every 30 seconds * **Pong Timeout**: Expect pong within 10 seconds * **Auto-disconnect**: After 24 hours (reconnect required) ```javascript // Heartbeat implementation const HEARTBEAT_INTERVAL = 30000; // 30 seconds function startHeartbeat(ws) { const interval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ id: "ping-" + Date.now(), method: "ping", params: {} })); } }, HEARTBEAT_INTERVAL); ws.addEventListener('close', () => { clearInterval(interval); }); } ``` ### Reconnection Strategy Implement automatic reconnection with exponential backoff: ```javascript class ReconnectingWebSocket { constructor(url) { this.url = url; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('Connected'); this.reconnectAttempts = 0; this.onopen?.(); }; this.ws.onclose = () => { this.reconnect(); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.onerror?.(error); }; this.ws.onmessage = (event) => { this.onmessage?.(event); }; } reconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); return; } const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); this.reconnectAttempts++; console.log(`Reconnecting in ${delay}ms...`); setTimeout(() => this.connect(), delay); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { throw new Error('WebSocket not connected'); } } } ``` ### Message Examples ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT" } } ``` ### Error Handling WebSocket errors require special handling: ```javascript function handleWebSocketError(error) { switch (error.code) { case 1006: console.error('Abnormal closure - network issue'); break; case 1008: console.error('Policy violation'); break; case 1011: console.error('Server error'); break; default: console.error('Unknown error:', error); } } ``` ### Security * All connections use WSS (WebSocket Secure) * Authentication required for trading endpoints * Rate limits apply to prevent abuse * IP-based connection limits ### Testing Test WebSocket connections using wscat: ```bash # Install wscat npm install -g wscat # Connect to info endpoint wscat -c wss://papi.synthetix.io/v1/ws/info # Send subscription > {"id":"sub-1","method":"subscribe","params":{"type":"orderbook","symbol":"BTC-USDT"}} ``` ### Common Issues | Issue | Cause | Solution | | --------------------- | ------------------- | -------------------------------- | | Connection drops | Network instability | Implement auto-reconnect | | No heartbeat response | Connection stale | Reconnect immediately | | Auth failures | Invalid signature | Check signing implementation | | Missing messages | No error handling | Add comprehensive error handlers | ### Next Steps * [Trade WebSocket](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Real-time trading * [Info WebSocket](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket) - Market data streams * [Timeouts & Heartbeats](https://developers.synthetix.io/developer-resources/api/ws-api/timeouts-heartbeats) - Connection management * [Authentication](https://developers.synthetix.io/developer-resources/api/rest-api/trade/authentication) - Signing implementation ## Timeouts & Heartbeats Proper timeout and heartbeat management is crucial for maintaining stable WebSocket connections to the Synthetix API. ### Connection Lifecycle 1. **Initial Connection**: TCP handshake and WebSocket upgrade 2. **Authentication**: 30-second window for trade endpoint 3. **Active State**: Send/receive messages with heartbeats 4. **Idle Timeout**: Connection closed after inactivity 5. **Max Duration**: Forced disconnect after 24 hours ### Heartbeat Protocol #### Client Heartbeat (Ping) Send a ping message every 30 seconds to keep the connection alive: ```json { "id": "ping-1704067200000", "method": "ping", "params": {} } ``` #### Server Response (Pong) The server responds with: ```json { "id": "ping-1704067200000", "status": 200, "result": { "message": "pong" } } ``` #### Implementation ```javascript class HeartbeatManager { constructor(ws) { this.ws = ws; this.pingInterval = 30000; // 30 seconds this.pongTimeout = 10000; // 10 seconds this.intervalId = null; this.pendingPings = new Map(); this.lastPong = Date.now(); } start() { this.intervalId = setInterval(() => { this.sendPing(); }, this.pingInterval); } sendPing() { if (this.ws.readyState !== WebSocket.OPEN) { return; } const pingId = `ping-${Date.now()}`; const pingTime = Date.now(); const ping = { id: pingId, method: 'ping', params: {} }; this.ws.send(JSON.stringify(ping)); // Set pong timeout const timeoutId = setTimeout(() => { this.pendingPings.delete(pingId); console.error('Pong timeout - connection may be dead'); this.ws.close(1001, 'Pong timeout'); }, this.pongTimeout); this.pendingPings.set(pingId, { pingTime, timeoutId }); } handlePong(message) { const pending = this.pendingPings.get(message.id); if (pending) { clearTimeout(pending.timeoutId); this.pendingPings.delete(message.id); this.lastPong = Date.now(); // Calculate round-trip time const rtt = Date.now() - pending.pingTime; console.log(`Heartbeat RTT: ${rtt}ms`); } } stop() { clearInterval(this.intervalId); this.pendingPings.forEach(({ timeoutId }) => clearTimeout(timeoutId)); this.pendingPings.clear(); } getLastPongAge() { return Date.now() - this.lastPong; } } ``` ### Timeout Configuration #### Connection Timeouts | Timeout Type | Duration | Description | | -------------- | ---------- | ---------------------------------- | | Connection | 30 seconds | Max time to establish WebSocket | | Authentication | 30 seconds | Time to authenticate after connect | | Request | 30 seconds | Max time for request response | | Idle | 5 minutes | Connection closed if no activity | | Session | 24 hours | Maximum connection duration | #### Request Timeouts Implement timeouts for all requests: ```javascript class RequestManager { constructor(ws) { this.ws = ws; this.requests = new Map(); this.defaultTimeout = 30000; // 30 seconds } sendRequest(request, timeout = this.defaultTimeout) { return new Promise((resolve, reject) => { const requestId = request.id; // Store request handler this.requests.set(requestId, { resolve, reject, timestamp: Date.now() }); // Set timeout const timeoutId = setTimeout(() => { const handler = this.requests.get(requestId); if (handler) { this.requests.delete(requestId); handler.reject(new Error(`Request timeout: ${requestId}`)); } }, timeout); // Store timeout ID for cleanup this.requests.get(requestId).timeoutId = timeoutId; // Send request this.ws.send(JSON.stringify(request)); }); } handleResponse(response) { const handler = this.requests.get(response.id); if (handler) { clearTimeout(handler.timeoutId); this.requests.delete(response.id); if (response.error) { handler.reject(new Error(response.error.message)); } else { handler.resolve(response.result); } } } cleanup() { // Clear all pending requests this.requests.forEach((handler) => { clearTimeout(handler.timeoutId); handler.reject(new Error('Connection closed')); }); this.requests.clear(); } } ``` ### Connection Management #### Complete Connection Manager ```javascript class WebSocketConnection { constructor(url) { this.url = url; this.ws = null; this.heartbeat = null; this.requestManager = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.sessionStartTime = null; this.isAuthenticated = false; } async connect() { return new Promise((resolve, reject) => { // Connection timeout const connectTimeout = setTimeout(() => { reject(new Error('Connection timeout')); this.ws?.close(); }, 30000); this.ws = new WebSocket(this.url); this.sessionStartTime = Date.now(); this.ws.onopen = () => { clearTimeout(connectTimeout); console.log('WebSocket connected'); // Initialize managers this.heartbeat = new HeartbeatManager(this.ws); this.requestManager = new RequestManager(this.ws); // Start heartbeat this.heartbeat.start(); // Start session timer this.startSessionTimer(); resolve(); }; this.ws.onclose = (event) => { this.handleClose(event); }; this.ws.onerror = (error) => { clearTimeout(connectTimeout); this.handleError(error); reject(error); }; this.ws.onmessage = (event) => { this.handleMessage(event); }; }); } handleMessage(event) { try { const message = JSON.parse(event.data); // Route to appropriate handler based on message structure if (message.id && (message.status !== undefined || message.error)) { // Response message: has id + status or error if (message.result && message.result.message === 'pong') { this.heartbeat.handlePong(message); } else { this.requestManager.handleResponse(message); } } else if (message.method && message.data) { // Push notification: has method + data this.handleNotification(message); } } catch (error) { console.error('Message parse error:', error); } } handleClose(event) { console.log(`WebSocket closed: ${event.code} - ${event.reason}`); // Cleanup this.heartbeat?.stop(); this.requestManager?.cleanup(); this.isAuthenticated = false; // Determine if should reconnect if (event.code !== 1000 && this.shouldReconnect()) { this.scheduleReconnect(); } } handleError(error) { console.error('WebSocket error:', error); } shouldReconnect() { // Don't reconnect if session expired if (this.isSessionExpired()) { console.log('Session expired - not reconnecting'); return false; } // Check reconnect attempts return this.reconnectAttempts < this.maxReconnectAttempts; } scheduleReconnect() { const delay = Math.min( this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000 // Max 30 seconds ); this.reconnectAttempts++; console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(async () => { try { await this.connect(); this.reconnectAttempts = 0; // Reset on success // Re-authenticate if needed if (this.url.includes('/trade')) { await this.authenticate(); } } catch (error) { console.error('Reconnection failed:', error); } }, delay); } startSessionTimer() { // Force disconnect after 24 hours const sessionTimeout = 24 * 60 * 60 * 1000; // 24 hours setTimeout(() => { console.log('Session expired - closing connection'); this.ws.close(1000, 'Session expired'); }, sessionTimeout); } isSessionExpired() { if (!this.sessionStartTime) return false; const sessionDuration = Date.now() - this.sessionStartTime; return sessionDuration >= 24 * 60 * 60 * 1000; } async authenticate() { // Implement authentication with timeout const authTimeout = 30000; // 30 seconds try { const authRequest = { id: 'auth-1', method: 'auth', params: { // Auth params (message and signature) } }; const result = await this.requestManager.sendRequest(authRequest, authTimeout); if (result && result.sub_account_id) { this.isAuthenticated = true; console.log('Authenticated successfully'); } else { throw new Error('Authentication failed'); } } catch (error) { console.error('Authentication error:', error); this.ws.close(1008, 'Authentication failed'); throw error; } } close() { this.heartbeat?.stop(); this.requestManager?.cleanup(); this.ws?.close(1000, 'Normal closure'); } getConnectionStats() { return { readyState: this.ws?.readyState, isAuthenticated: this.isAuthenticated, sessionAge: Date.now() - this.sessionStartTime, lastPongAge: this.heartbeat?.getLastPongAge(), reconnectAttempts: this.reconnectAttempts, pendingRequests: this.requestManager?.requests.size || 0 }; } } ``` ### Monitoring Connection Health ```javascript class ConnectionMonitor { constructor(connection) { this.connection = connection; this.interval = null; } start() { this.interval = setInterval(() => { const stats = this.connection.getConnectionStats(); // Check connection health if (stats.readyState !== WebSocket.OPEN) { console.warn('Connection not open:', stats.readyState); } // Check heartbeat if (stats.lastPongAge > 60000) { console.error('No pong received for 60s - connection may be dead'); } // Check session age const hoursConnected = stats.sessionAge / (1000 * 60 * 60); if (hoursConnected > 23) { console.warn(`Session approaching 24h limit: ${hoursConnected.toFixed(1)}h`); } // Log stats console.log('Connection stats:', { ...stats, sessionAge: `${(stats.sessionAge / 1000).toFixed(0)}s`, lastPongAge: `${(stats.lastPongAge / 1000).toFixed(0)}s` }); }, 30000); // Check every 30 seconds } stop() { clearInterval(this.interval); } } ``` ### Implementation Notes #### Heartbeat Implementation Application-level heartbeats enable connection monitoring: ```javascript // Start heartbeat immediately after connection ws.onopen = () => { heartbeat.start(); }; ``` #### 2. Handle All Timeout Scenarios ```javascript const timeouts = { connection: 30000, authentication: 30000, request: 30000, heartbeat: 30000, pong: 10000 }; ``` #### 3. Graceful Shutdown ```javascript async function shutdown(connection) { console.log('Shutting down gracefully...'); // Cancel pending requests connection.requestManager.cleanup(); // Stop heartbeat connection.heartbeat.stop(); // Close connection connection.close(); console.log('Shutdown complete'); } ``` #### 4. Monitor Connection Quality Track metrics to identify issues: * Round-trip time (RTT) * Missed pongs * Reconnection frequency * Request timeout rate ### Common Issues | Issue | Cause | Solution | | -------------------- | ------------------- | -------------------------------------- | | Frequent disconnects | Network instability | Implement robust reconnection | | Pong timeouts | High latency | Increase pong timeout | | Session expiry | 24-hour limit | Plan for reconnection | | Stuck requests | No timeout | Request timeout configuration required | ### Next Steps * [WebSocket Overview](https://developers.synthetix.io/developer-resources/api/ws-api) - WebSocket basics * [Trade WebSocket](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Trading implementation * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Timeout error patterns import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Candles Retrieve OHLCV (Open, High, Low, Close, Volume) price data for a specific trading pair. ### Request #### Request Format ```json { "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "1h", "limit": 500, "startTime": 1704067200000, "endTime": 1704153600000 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `"getCandles"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., "BTC-USDT") | | `params.interval` | string | Yes | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `8h`, `12h`, `1d`, `3d`, `1w`, `1M`, `3M` | | `params.limit` | integer | No | Number of candles to return (0 = no limit) | | `params.startTime` | integer | No | Start time in milliseconds (default: 24 hours ago) | | `params.endTime` | integer | No | End time in milliseconds (default: current time) | #### Valid Intervals | Interval | Description | Milliseconds | | -------- | ----------- | ------------ | | `1m` | 1 minute | 60,000 | | `5m` | 5 minutes | 300,000 | | `15m` | 15 minutes | 900,000 | | `30m` | 30 minutes | 1,800,000 | | `1h` | 1 hour | 3,600,000 | | `4h` | 4 hours | 14,400,000 | | `8h` | 8 hours | 28,800,000 | | `12h` | 12 hours | 43,200,000 | | `1d` | 1 day | 86,400,000 | | `3d` | 3 days | 259,200,000 | | `1w` | 1 week | 604,800,000 | | `1M` | 1 month | variable | | `3M` | 3 months | variable | ### Response #### Success Response ```json { "status": "ok", "response": { "symbol": "BTC-USDT", "interval": "1h", "candles": [ { "openTime": 1704067200000, "closeTime": 1704070800000, "openPrice": "45000.50", "highPrice": "45100.00", "lowPrice": "44950.00", "closePrice": "45050.00", "volume": "125.5", "quoteVolume": "125500.00", "tradeCount": 1523 }, { "openTime": 1704070800000, "closeTime": 1704074400000, "openPrice": "45050.00", "highPrice": "45200.00", "lowPrice": "45000.00", "closePrice": "45150.00", "volume": "98.2", "quoteVolume": "98450.00", "tradeCount": 1102 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Candle Response Object | Field | Type | Description | | ---------- | ------ | ----------------------- | | `symbol` | string | Trading pair symbol | | `interval` | string | Time interval used | | `candles` | array | Array of candle objects | #### Candle Object | Field | Type | Description | | ------------- | ------- | --------------------------------- | | `openTime` | integer | Candle open time in milliseconds | | `closeTime` | integer | Candle close time in milliseconds | | `openPrice` | string | Opening price | | `highPrice` | string | Highest price during the period | | `lowPrice` | string | Lowest price during the period | | `closePrice` | string | Closing price | | `volume` | string | Base asset trading volume | | `quoteVolume` | string | Quote asset trading volume | | `tradeCount` | integer | Number of trades in the period | #### Error Response ```json { "status": "error", "error": { "message": "Invalid request parameters", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get Recent 1-Hour Candles ```json { "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "1h", "limit": 100 } } ``` #### Get Daily Candles for Date Range ```json { "params": { "action": "getCandles", "symbol": "ETH-USDT", "interval": "1d", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 30 } } ``` ### Candle Data Information * **OHLCV Format**: Standard Open, High, Low, Close, Volume data * **Flexible Timeframes**: Multiple intervals from 1 minute to 3 months * **Configurable Limits**: Set limit to 0 for no limit, or specify desired number of candles * **Time Range Support**: Optional start and end time filtering * **Real-time Data**: Latest market data with minimal delay ### Use Cases * **Technical Analysis**: Chart patterns, indicators, and analysis * **Trading Strategies**: Backtesting and strategy development * **Market Research**: Historical price movement analysis * **Risk Management**: Volatility and trend analysis * **Performance Tracking**: Portfolio performance over time ### Validation Rules * `symbol` must be a valid trading pair * `interval` must be one of the valid intervals * `limit` must be non-negative (≥ 0), where 0 means no limit * `startTime` must be less than `endTime` if both provided * No authentication required ### Error Handling Common error scenarios: | Error | Description | | ----------------------- | ---------------------------- | | Symbol required | Missing trading pair symbol | | Invalid interval | Unsupported time interval | | Invalid limit | Limit must be non-negative | | Invalid time range | Start time is after end time | | Failed to retrieve data | Internal service error | ### Rate Limiting * **Default**: 1000 requests per minute per IP * **Burst**: Up to 100 requests per second * **Data Volume**: Large requests may be rate-limited ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Market configuration * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current prices * [Get Orderbook](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) - Order book depth import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import InfoEndpoints from "../../../../../snippets/info-endpoints.mdx"; ## Get Collaterals Retrieve configuration for all supported collateral assets, including deposit caps, LTV thresholds, withdrawal fees, and tiered haircut structures. ### Request #### Request Format ```json { "params": { "action": "getCollaterals" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | -------------------------- | | `params.action` | string | Yes | Must be `"getCollaterals"` | ### Response #### Success Response ```json { "status": "ok", "response": [ { "collateral": "USDT", "market": "USDTUSD", "depositCap": "10000000.00000000", "ltv": "0.95", "lltv": "0.98", "withdrawFee": "5.00000000", "tiers": [ { "id": 1, "name": "Tier1", "minAmount": "0.00000000", "maxAmount": "100000.00000000", "valueRatio": "1.00000000", "haircut": "0.00000000", "valueAddition": "0.00000000" }, { "id": 2, "name": "Tier2", "minAmount": "100000.00000000", "maxAmount": "", "valueRatio": "0.99500000", "haircut": "0.00500000", "valueAddition": "0.00000000" } ] }, { "collateral": "ETH", "market": "ETHUSD", "depositCap": "5000.00000000", "ltv": "0.90", "lltv": "0.95", "withdrawFee": "5.00000000", "tiers": [ { "id": 3, "name": "Tier1", "minAmount": "0.00000000", "maxAmount": "50000.00000000", "valueRatio": "0.98000000", "haircut": "0.02000000", "valueAddition": "0.00000000" } ] } ], "requestId": "5ccf215d37e3ae6d" } ``` #### Collateral Object Fields | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------------- | | `collateral` | string | Collateral asset symbol (e.g., "USDT", "ETH", "WBTC") | | `market` | string | Price feed market used to value this collateral (e.g., "USDTUSD", "ETHUSD") | | `depositCap` | string | Platform-wide maximum deposit limit for this collateral type (decimal string) | | `ltv` | string | Loan-to-Value ratio - target after auto-exchange (e.g., "0.95" for 95%) | | `lltv` | string | Liquidation LTV - threshold that triggers auto-exchange to USDT (e.g., "0.98" for 98%) | | `withdrawFee` | string | Flat fee in USD charged when withdrawing this collateral (decimal string) | | `tiers` | array | Array of collateral tier configurations for tiered haircut structure | #### Tier Object Fields | Field | Type | Description | | --------------- | ------- | ------------------------------------------------------------------------- | | `id` | integer | Unique tier identifier | | `name` | string | Tier name (e.g., "Tier1", "Tier2") | | `minAmount` | string | Minimum amount in USDT for this tier (decimal string) | | `maxAmount` | string | Maximum amount in USDT for this tier (decimal string, empty if unlimited) | | `valueRatio` | string | Collateral value ratio (e.g., "0.995" for 99.5% value) | | `haircut` | string | Collateral haircut percentage (e.g., "0.01" for 1% haircut) | | `valueAddition` | string | Fixed value addition in USDT (decimal string) | #### Error Response ```json { "status": "error", "error": { "code": "INTERNAL_ERROR", "message": "Could not retrieve collateral configuration", "details": {} }, "requestId": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Collaterals ```json { "params": { "action": "getCollaterals" } } ``` ### Collateral Information * **No Authentication Required**: Public endpoint accessible without signing * **Supported Assets**: Returns all collateral types supported by the platform * **Tiered Haircuts**: Larger collateral holdings may have larger haircuts applied * **Auto-Exchange**: Non-USDT collateral may be auto-exchanged when LLTV threshold is breached ### Use Cases * **Deposit Planning**: Check deposit caps and supported assets before depositing * **Withdrawal Validation**: Verify withdrawal fees for each collateral type * **Risk Assessment**: Understand LTV/LLTV thresholds for non-USDT collateral * **Collateral Valuation**: Calculate effective collateral value using tier haircuts ### Validation Rules * No authentication required * No rate limiting beyond standard API limits * Returns data for all configured collateral assets ### Error Handling Common error scenarios: | Error | Description | | ------------------------------------ | ------------------------------------------------------------- | | Collateral configuration unavailable | Failed to retrieve collateral data from configuration service | | Internal error | Service temporarily unavailable | ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Market configuration * [Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/rest-api/trade/withdrawCollateral) - Withdraw collateral from subaccount * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - View collateral balances import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import FundingRateObject from '../../../../../snippets/funding-rate-object.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; ## Get Funding Rate Retrieve current funding rates for perpetual markets. This is a public endpoint that returns market-wide funding rate information without requiring authentication. ### Request #### Request Format ```json { "params": { "action": "getFundingRate", "symbol": "BTC-USDT" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `"getFundingRate"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Normalized to canonical uppercase form — lowercase input (e.g., `"btc-usdt"`) is accepted and normalized automatically. | #### Example Request ##### cURL ```bash curl -X POST https://papi.synthetix.io/info \ -H "Content-Type: application/json" \ -d '{ "params": { "action": "getFundingRate", "symbol": "BTC-USDT" } }' ``` ##### JavaScript ```javascript const response = await fetch('https://papi.synthetix.io/info', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ params: { action: 'getFundingRate', symbol: 'BTC-USDT' } }) }); const data = await response.json(); console.log(data); ``` ##### Python ```python import requests response = requests.post('https://papi.synthetix.io/info', json={ "params": { "action": "getFundingRate", "symbol": "BTC-USDT" } } ) data = response.json() print(data) ``` ### Response #### Success Response ```json { "status": "ok", "response": { "symbol": "BTC-USDT", "estimatedFundingRate": "0.000010960225996", "lastSettlementRate": "0.00001250", "lastSettlementTime": 1735689600000, "nextFundingTime": 1735693200000, "fundingInterval": 3600000 }, "request_id": "5ccf215d37e3ae6d" } ``` **Note on `lastSettlementTime`**: For newly listed markets that have not yet had a funding settlement, `lastSettlementTime` is returned as `0`. Check for `0` before interpreting this value as a real settlement timestamp. #### Funding Rate Calculation The funding rate is calculated using: * **Premium Rate**: Difference between perpetual and spot prices * **Interest Rate**: Typically 0.01% per 1-hour period * **Dampening Factor**: Applied to smooth rate changes **Formula**: `Funding Rate = Premium Rate + clamp(Interest Rate - Premium Rate, -0.05%, 0.05%)` #### Understanding Funding Payments * **Positive funding rate**: Long positions pay short positions * **Negative funding rate**: Short positions pay long positions * **Payment frequency**: Every 1 hour (hourly basis) * **Rate basis**: 1-hour rate (multiply by 24 for daily, 1095 for annual) ### Error Responses #### Endpoint-Specific Errors | HTTP Status | Error Code | Description | Solution | | ----------- | ------------------ | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | Symbol is missing, empty, or not a valid market symbol format | Provide a valid symbol (e.g., `"BTC-USDT"`). Symbols are accepted in any case and normalized automatically. | | 404 | `NOT_FOUND` | No funding rate data found for the requested symbol | Verify the symbol is an active market using [getMarkets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) | ### Use Cases #### Real-Time Funding Monitoring ```javascript // Monitor funding rates for risk management const symbols = ['BTC-USDT', 'ETH-USDT', 'SOL-USDT']; const fundingRates = await Promise.all( symbols.map(symbol => getFundingRate(symbol)) ); ``` #### Funding Rate Alerts ```javascript // Set up alerts for extreme funding rates if (Math.abs(parseFloat(fundingRate)) > 0.01) { console.log(`High funding rate alert: ${symbol} at ${fundingRate}`); } ``` ### Related Endpoints * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) - User's funding payments history * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Available trading pairs * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current market prices import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; ## Get Funding Rate History Retrieve historical funding rates for a perpetual market over a specified time range. This is a public endpoint — no authentication required. ### Request #### Request Format ```json { "params": { "action": "getFundingRateHistory", "symbol": "BTC-USDT", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 100 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `"getFundingRateHistory"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Normalized to canonical uppercase form — lowercase input is accepted and normalized automatically. | | `params.startTime` | integer | Yes | Start time in milliseconds since epoch. Must be non-zero and no more than 30 days in the past. | | `params.endTime` | integer | Yes | End time in milliseconds since epoch. Must be non-zero and after `startTime`. Cannot be more than 30 days in the past. | | `params.limit` | integer | No | Maximum number of records to return. If omitted, non-positive, or above `2160`, the server uses the cap of `2160`. | #### Validation Rules * `symbol` must be a valid, supported trading pair * `startTime` is required, must be non-zero, and cannot be more than 30 days in the past * `endTime` is required, must be non-zero, and cannot be more than 30 days in the past * `startTime` must be strictly before `endTime` #### Example Request ##### cURL ```bash curl -X POST https://papi.synthetix.io/v1/info \ -H "Content-Type: application/json" \ -d '{ "params": { "action": "getFundingRateHistory", "symbol": "BTC-USDT", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 100 } }' ``` ##### JavaScript ```javascript const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'getFundingRateHistory', symbol: 'BTC-USDT', startTime: Date.now() - 24 * 60 * 60 * 1000, endTime: Date.now(), limit: 100, }, }), }); const data = await response.json(); console.log(data.response.fundingRates); ``` ##### Python ```python import requests import time response = requests.post('https://papi.synthetix.io/v1/info', json={ "params": { "action": "getFundingRateHistory", "symbol": "BTC-USDT", "startTime": int((time.time() - 86400) * 1000), "endTime": int(time.time() * 1000), "limit": 100, } } ) data = response.json() for record in data["response"]["fundingRates"]: print(record["fundingTime"], record["fundingRate"]) ``` ### Response #### Success Response ```json { "status": "ok", "response": { "symbol": "BTC-USDT", "fundingRates": [ { "fundingRate": "0.00001250", "fundingTime": 1704153600000, "appliedAt": 1704153660000 }, { "fundingRate": "0.000010960225996", "fundingTime": 1704150000000, "appliedAt": 1704150060000 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Object | Field | Type | Description | | -------------- | ------ | ------------------------------------------- | | `symbol` | string | Trading pair symbol | | `fundingRates` | array | Array of funding rate records, newest first | #### Funding Rate Record | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------------- | | `fundingRate` | string | The published funding rate for the period | | `fundingTime` | integer | When the rate was published (milliseconds since epoch) | | `appliedAt` | integer | When the rate was applied to positions (milliseconds since epoch) | ### Error Responses #### Endpoint-Specific Errors | HTTP Status | Error Code | Description | Solution | | ----------- | ------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | 400 | `INVALID_FORMAT` | Request body could not be decoded | Check request JSON structure | | 400 | `VALIDATION_ERROR` | Invalid symbol, missing or zero time value, or `startTime` >= `endTime` | Use a valid symbol from [getMarkets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) and ensure `startTime` \< `endTime` | | 400 | `VALIDATION_ERROR` | `startTime` or `endTime` older than 30 days | Both timestamps must be within the last 30 days | ### Use Cases #### Average funding rate over a time window ```javascript const now = Date.now(); const oneDayAgo = now - 24 * 60 * 60 * 1000; const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'getFundingRateHistory', symbol: 'BTC-USDT', startTime: oneDayAgo, endTime: now, }, }), }); const { response: { fundingRates } } = await response.json(); const avgRate = fundingRates.reduce((sum, r) => sum + parseFloat(r.fundingRate), 0) / fundingRates.length; console.log(`Average funding rate (24h): ${avgRate}`); ``` ### Related Endpoints * [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) - Current funding rate for a market * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) - Your account's funding payment history * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Available trading pairs import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; ## Get Is Whitelisted Check if a wallet address is whitelisted to place orders on the platform. This is a public endpoint that returns whether a specific wallet address has trading permissions. ### Request #### Request Format ```json { "params": { "action": "getIsWhitelisted", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------------------------------ | | `params.action` | string | Yes | Must be `"getIsWhitelisted"` | | `params.walletAddress` | string | Yes | Ethereum wallet address to check (hex format, e.g., "0x742d35...") | #### Example Request ##### cURL ```bash curl -X POST https://papi.synthetix.io/v1/info \ -H "Content-Type: application/json" \ -d '{ "params": { "action": "getIsWhitelisted", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" } }' ``` ##### JavaScript ```javascript const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ params: { action: 'getIsWhitelisted', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0' } }) }); const data = await response.json(); console.log(data); ``` ##### Python ```python import requests response = requests.post('https://papi.synthetix.io/v1/info', json={ "params": { "action": "getIsWhitelisted", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" } } ) data = response.json() print(data) ``` ### Response #### Success Response ```json { "status": "ok", "response": true, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Format The response contains a boolean value: * `true` - Wallet is whitelisted and can place orders * `false` - Wallet is not whitelisted and cannot place orders #### Response Object | Field | Type | Description | | ---------- | ------- | -------------------------------------------------- | | `response` | boolean | `true` if wallet is whitelisted, `false` otherwise | ### Validation Rules * `walletAddress` must be provided and not empty * `walletAddress` must not exceed 42 characters * `walletAddress` must be a valid Ethereum hex address format (0x followed by 40 hexadecimal characters) * Address format is case-insensitive (normalized to lowercase internally) * No authentication required ### Error Handling Common error scenarios: #### Endpoint-Specific Errors | Error | Description | | ----------------------------------------------------- | ------------------------------------------------------------------ | | Invalid request body | Missing or empty `walletAddress` parameter | | walletAddress exceeds maximum length of 42 characters | `walletAddress` string is longer than the maximum of 42 characters | | Invalid wallet address format | Address is not a valid Ethereum hex address | | Whitelist arbitrator unavailable | Internal service error, retry request | | Failed to obtain whitelist arbitration | Temporary service issue, retry request | #### Error Response Examples ##### Invalid Wallet Address ```json { "status": "error", "error": { "message": "Invalid wallet address format", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ##### Missing Parameter ```json { "status": "error", "error": { "message": "Invalid request body", "code": "INVALID_FORMAT" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Use Cases #### Pre-Trading Validation ```javascript // Check if user is whitelisted before allowing trading UI access async function checkTradingAccess(walletAddress) { const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'getIsWhitelisted', walletAddress: walletAddress } }) }); const data = await response.json(); if (data.status === 'ok' && data.response === true) { console.log('User has trading access'); return true; } else { console.log('User needs to complete whitelist process'); return false; } } ``` #### Bulk Whitelist Check ```javascript // Check multiple wallets at once async function checkMultipleWallets(walletAddresses) { const results = await Promise.all( walletAddresses.map(async (address) => { const response = await getIsWhitelisted(address); return { address, isWhitelisted: response.response }; }) ); return results; } ``` #### Access Control ```javascript // Gate trading features based on whitelist status if (await isWhitelisted(userWallet)) { // Show trading interface enableTradingFeatures(); } else { // Show whitelist application flow showWhitelistApplication(); } ``` ### Understanding Whitelisting #### What is Whitelisting? Whitelisting is an access control mechanism that determines which wallet addresses are permitted to place orders on the platform. This helps ensure: * Regulatory compliance * Risk management * Platform security * User verification requirements #### Getting Whitelisted If a wallet is not whitelisted, users typically need to: 1. Complete KYC/verification requirements 2. Meet platform criteria 3. Apply through the official whitelist process 4. Wait for approval Contact platform support or check the main website for whitelist application procedures. ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Available trading pairs * [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - Place trading orders (requires whitelisted wallet) * [Get Sub Account IDs](https://developers.synthetix.io/developer-resources/api/rest-api/info/getSubAccountIds) - Get sub-accounts for a wallet import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; ## Get Last Trades Retrieve the most recent trade execution history (fills) from Synthetix's orderbook across all users. Each trade represents a filled order or partial fill that has been executed. This endpoint provides public market data without requiring authentication. ### Request #### Request Format ```json { "params": { "action": "getLastTrades", "symbol": "BTC-USDT", "limit": 100 } } ``` ### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `"getLastTrades"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Normalized to uppercase; maximum 20 characters. | | `params.limit` | integer | No | Maximum number of trades to return (default: 50, min: 1, max: 100) | #### No Authentication Required This is a public endpoint that does not require authentication. ### Response #### Success Response ```json { "status": "ok", "response": { "trades": [ { "tradeId": "123456789", "symbol": "BTC-USDT", "side": "buy", "price": "50000.50", "quantity": "0.1", "timestamp": 1704067200500, "isMaker": false }, { "tradeId": "123456788", "symbol": "BTC-USDT", "side": "sell", "price": "50000.25", "quantity": "0.05", "timestamp": 1704067199800, "isMaker": true } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Trade Object (Public) | Field | Type | Description | | ----------- | ------- | ------------------------------------------------ | | `tradeId` | string | Unique identifier for the trade | | `symbol` | string | Market symbol (e.g., `"BTC-USDT"`) | | `side` | string | Order side: `"buy"` or `"sell"` | | `price` | string | Execution price as string | | `quantity` | string | Executed quantity as string | | `timestamp` | integer | Execution time in milliseconds since epoch | | `isMaker` | boolean | Whether this was a maker order (added liquidity) | #### Error Response ```json { "status": "error", "error": { "message": "Invalid symbol", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Examples #### Get Recent Trades with Default Limit ```json { "params": { "action": "getLastTrades", "symbol": "BTC-USDT" } } ``` #### Filter by Symbol ```json { "params": { "action": "getLastTrades", "symbol": "ETH-USDT", "limit": 100 } } ``` #### Maximum Results ```json { "params": { "action": "getLastTrades", "symbol": "BTC-USDT", "limit": 100 } } ``` ### Response Fields #### Response Structure | Field | Type | Description | | -------- | ----- | ----------------------------- | | `trades` | array | Array of public trade objects | #### Sorting Trades are returned in descending order by timestamp (newest first). ### Implementation Notes * **Public Data**: This endpoint provides public market data suitable for price discovery, trading interfaces, market data feeds, and public trade history displays * **Symbol Required**: A valid market symbol must be provided * **Limit Usage**: Default limit is 50 trades, minimum 1, maximum 100 trades per request * **Caching**: Trade data is immutable once created, suitable for short-term caching * **Real-time Updates**: Use Trade Updates WebSocket subscription for live data with sub-50ms latency. Use REST for initial load, WebSocket for ongoing updates ### Use Cases #### Market Analysis * Recent price discovery and trade flow analysis * Volume and liquidity assessment * Market depth analysis when combined with orderbook data #### Trading Interfaces * Displaying recent market activity * Trade history components * Price movement indicators #### Data Feeds * Building market data aggregators * Feeding external analytics systems * Creating public market displays ### Differences from getTrades | Feature | getLastTrades (Public) | getTrades (Private) | | -------------------- | ---------------------- | ------------------------------- | | **Authentication** | Not required | Required (EIP-712 signature) | | **Data Scope** | All users | Specific subaccount only | | **Sensitive Data** | Excluded | Included (fees, PnL, order IDs) | | **Symbol Parameter** | Required | Optional | | **Pagination** | Limit only | Limit + offset | | **Time Filtering** | Not supported | Start/end time supported | | **Rate Limits** | More permissive | Standard authenticated limits | | **Maximum Limit** | 100 trades | 1000 trades | ### Error Handling #### Common Errors | Error | Description | Solution | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `symbol is required` | Symbol parameter is missing or empty | Provide a valid market symbol | | `invalid symbol` | Market symbol format is not recognized | Use uppercase format, e.g. `"BTC-USDT"` | | `symbol exceeds maximum length of 20 characters` | Symbol exceeds 20-character limit | Shorten or correct the symbol value | | `Limit cannot exceed 100` | Limit exceeds maximum | Use limit ≤ 100 | | `Limit must be at least 1` | Limit is less than 1 | Use limit ≥ 1 | | `Market not found` | Specified symbol doesn't exist | Verify symbol format and availability | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### Rate Limiting * Public endpoints typically have higher rate limits * Each request counts as 1 against rate limits * No authentication overhead improves response times * See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for details ### Next Steps * [Get Last Trades (WebSocket)](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getLastTrades) - WebSocket version for lower latency * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Available trading markets * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current market prices * [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - Your account's trade history * Trade Updates - Real-time trade stream * [WebSocket Info](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket) - Real-time public market data import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import MarketPriceObject from '../../../../../snippets/market-price-object.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Market Prices Retrieve current market prices and 24-hour statistics for all trading pairs. ### Request #### Request Format ```json { "params": { "action": "getMarketPrices" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | --------------------------- | | `params.action` | string | Yes | Must be `"getMarketPrices"` | ### Response #### Success Response ```json { "status": "ok", "response": { "BTC-USDT": { "symbol": "BTC-USDT", "markPrice": "45025.37500000", "indexPrice": "45025.50000000", "lastPrice": "45025.00000000", "bestBid": "45020.00000000", "bestAsk": "45030.00000000", "volume24h": "1250.50", "quoteVolume24h": "56282500.00", "fundingRate": "0.00001250", "openInterest": "12500.75", "prevDayPrice": "44952.00000000", "timestamp": 1704067200000 }, "ETH-USDT": { "symbol": "ETH-USDT", "markPrice": "2998.75000000", "indexPrice": "2998.80000000", "lastPrice": "2998.75000000", "bestBid": "2996.00000000", "bestAsk": "3001.50000000", "volume24h": "8945.25", "quoteVolume24h": "26800000.00", "fundingRate": "0.00001250", "openInterest": "45678.50", "prevDayPrice": "2804.00000000", "timestamp": 1704067200000 } }, "request_id": "5ccf215d37e3ae6d" } ``` #### Market Prices Response Object The response returns an object (map) where keys are market symbols and values are market price data: * **Object Format**: Keys are market symbols (e.g., "BTC-USDT"), enabling O(1) lookups * **Combined Data**: Each value includes real-time prices and rolling 24-hour statistics * **Symbol Included**: Each object contains the symbol field for consistency * **Fast Access**: Direct access to specific market data without iteration #### Error Response ```json { "status": "error", "error": { "message": "Could not get market prices", "code": "INTERNAL_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Market Prices ```json { "params": { "action": "getMarketPrices" } } ``` ### Validation Rules * No authentication required * No parameters to validate * Returns data for all available markets * Real-time data from price feeds ### Error Handling Common error scenarios: | Error | Description | | -------------------------------- | ----------------------------------- | | Market configuration unavailable | Failed to retrieve market list | | Mark price unavailable | Failed to retrieve mark price data | | Index price unavailable | Failed to retrieve index price data | ### Rate Limiting * **Default**: 1000 requests per minute per IP * **Burst**: Up to 100 requests per second * **Real-time Data**: High-frequency access supported ### Data Fields Explained #### Price Types ##### Mark Price The mark price is the current fair value of the perpetual contract used for: * Position valuation in portfolios * Liquidation price calculations * PnL calculations * Updated continuously based on trading activity and price feeds ##### Index Price The index price is a reference price derived from external spot exchanges: * Provides an external price reference independent of Synthetix trading * Used as a baseline for mark price calculations * Sourced from multiple spot exchanges for accuracy ##### Prev Day Price The market (last) price 24 hours ago #### Core Metrics ##### Volume * **volume24h**: Total base asset traded in 24 hours (e.g., BTC amount) * **quoteVolume24h**: Total quote asset traded in 24 hours (e.g., USDT amount) * Rolling 24-hour window, continuously updated ##### Funding Rate * **fundingRate**: Current estimated funding rate for the market, fetched from the funding rate service * For per-symbol funding rate history, see [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) ##### Open Interest * **openInterest**: Total open positions in base asset * Key indicator of market activity and liquidity #### Timestamps * **timestamp**: Unix timestamp in milliseconds representing when the response was generated * Each price field (markPrice, indexPrice, lastPrice) is updated in real-time from price feeds ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Market configuration * [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) - Current funding rates per symbol * [Get Open Interest](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOpenInterest) - Open interest for all markets * [Get Candles](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) - Historical price data * [Market Price Updates (WebSocket)](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time streaming of all price data import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import MarketObject from '../../../../../snippets/market-object.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Markets Retrieve comprehensive market configuration information for all available trading pairs. ### Request #### Request Format ```json { "params": { "action": "getMarkets" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------- | ------- | -------- | -------------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `"getMarkets"` | | `params.activeOnly` | boolean | No | If `true`, returns only markets where `isOpen: true`. Default: `false` (returns all markets) | ### Response #### Success Response ```json { "status": "ok", "response": [ { "symbol": "SOL-USDT", "description": "Solana", "baseAsset": "SOL", "quoteAsset": "USDT", "isOpen": true, "isCloseOnly": false, "priceExponent": 2, "quantityExponent": 2, "priceIncrement": "0.01", "minOrderSize": "0.01", "orderSizeIncrement": "0.01", "contractSize": 1, "maxMarketOrderSize": "1000000", "maxLimitOrderSize": "1000000", "minOrderPrice": "0.01", "limitOrderPriceCapRatio": "1.5", "limitOrderPriceFloorRatio": "0.5", "marketOrderPriceCapRatio": "1.1", "marketOrderPriceFloorRatio": "0.9", "liquidationClearanceFee": "0.002", "minNotionalValue": "10", "maintenanceMarginTiers": [ { "minPositionSize": "0", "maxPositionSize": "500000", "maxLeverage": 100, "initialMarginRequirement": "0.01", "maintenanceMarginRequirement": "0.005", "maintenanceDeductionValue": "0" }, { "minPositionSize": "500001", "maxPositionSize": "10000000", "maxLeverage": 50, "initialMarginRequirement": "0.02", "maintenanceMarginRequirement": "0.01", "maintenanceDeductionValue": "0" }, { "minPositionSize": "10000001", "maxPositionSize": "50000000", "maxLeverage": 25, "initialMarginRequirement": "0.04", "maintenanceMarginRequirement": "0.02", "maintenanceDeductionValue": "0" }, { "minPositionSize": "50000001", "maxPositionSize": "200000000", "maxLeverage": 10, "initialMarginRequirement": "0.1", "maintenanceMarginRequirement": "0.05", "maintenanceDeductionValue": "0" }, { "minPositionSize": "200000001", "maxPositionSize": "", "maxLeverage": 2, "initialMarginRequirement": "0.5", "maintenanceMarginRequirement": "0.25", "maintenanceDeductionValue": "0" } ] } ], "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "code": "INTERNAL_ERROR", "message": "Could not pull market configuration", "details": {} }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Markets ```json { "params": { "action": "getMarkets" } } ``` #### Get Only Active Markets ```json { "params": { "action": "getMarkets", "activeOnly": true } } ``` ### Market Information * **Comprehensive Data**: Includes all market configuration, trading rules, and limits * **Real-time Updates**: Market status and configuration are updated in real-time * **Trading Rules**: All necessary information for placing compliant orders * **Margin Requirements**: Complete margin tier information for risk management * **Price Precision**: Exact decimal precision for prices and quantities ### Use Cases * **Trading Applications**: Get market specifications for order validation * **Risk Management**: Access margin requirements and position limits * **Market Analysis**: Understand trading rules and market structure * **Order Management**: Validate order parameters before submission * **System Integration**: Configure trading systems with market rules ### Validation Rules * No authentication required * No rate limiting beyond standard API limits * Returns data for all available markets ### Error Handling Common error scenarios: | Error | Description | | -------------------------------- | --------------------------------------------------------- | | Market configuration unavailable | Failed to retrieve market data from configuration service | | Internal error | Service temporarily unavailable | ### Related Endpoints * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current market prices * [Get Orderbook](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) - Order book depth import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; ## Get Mid Prices Retrieve the current mid prices for all available markets. Mid prices represent the midpoint between the best bid and best ask prices, providing a simple reference price for each market. ### Request #### Request Format ```json { "params": { "action": "getMids" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------- | | `params.action` | string | Yes | Must be `"getMids"` | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------ | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Map of market symbols to mid prices (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response ```json { "status": "ok", "response": { "BTC-USDT": "99974.25000000", "ETH-USDT": "3498.87500000", "SOL-USDT": "159.82500000", "DOGE-USDT": "0.18650000" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ---------- | ------ | ------------------------------------------------------------- | | `{symbol}` | string | Mid price for the specified market symbol as a decimal string | #### Error Response ```json { "status": "error", "error": { "code": "INTERNAL_ERROR", "message": "Could not pull market configuration" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Implementation Notes * Returns mid prices for **all active/open markets** in a single request (markets where `isOpen: true`) * Mid price is calculated from the latest price feed data * Prices are returned as decimal strings to preserve precision * No filtering or symbol selection - all open markets are always returned * For real-time mid price updates, use the [`marketPriceUpdates`](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) WebSocket subscription * For comprehensive price data (mark, index, best bid/ask, etc.), use [`getMarketPrices`](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) ### Use Cases | Use Case | Description | | ------------------- | -------------------------------------------------------------- | | **Price Overview** | Quick snapshot of current prices across all markets | | **Initial Load** | Bootstrap client-side price data before subscribing to updates | | **Reconciliation** | Verify client price cache against server state | | **Simple Displays** | Show reference prices without full market data | ### Comparison with Other Price Endpoints | Endpoint | Data Returned | Use Case | | ---------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------- | | **getMids** | Simple mid prices only | Quick price snapshot | | [`getMarketPrices`](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) | Mark, index, last, bid/ask, funding, volume, OI | Comprehensive market data | | [`marketPriceUpdates`](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) | Real-time price updates | Live price tracking | ### Error Handling Common error scenarios: | Error | Description | | ----------------------------------- | ----------------------------------------------------- | | Could not pull market configuration | Failed to retrieve list of available markets | | Could not get mid price | Failed to retrieve price data for one or more markets | | Internal error | Server-side processing error | ### Example Usage #### JavaScript/TypeScript ```javascript const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ params: { action: 'getMids' } }) }); const data = await response.json(); if (data.status === 'ok') { const midPrices = data.response; console.log('BTC mid price:', midPrices['BTC-USDT']); console.log('ETH mid price:', midPrices['ETH-USDT']); } else { console.error('Error:', data.error); } ``` #### Python ```python import requests response = requests.post( 'https://papi.synthetix.io/v1/info', json={'params': {'action': 'getMids'}} ) data = response.json() if data['status'] == 'ok': mid_prices = data['response'] print(f"BTC mid price: {mid_prices['BTC-USDT']}") print(f"ETH mid price: {mid_prices['ETH-USDT']}") else: print(f"Error: {data['error']}") ``` #### cURL ```bash curl -X POST https://papi.synthetix.io/v1/info \ -H "Content-Type: application/json" \ -d '{"params": {"action": "getMids"}}' ``` import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import OpenInterestObject from '../../../../../snippets/open-interest-object.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Open Interest Retrieve open interest data for all trading markets. ### Request #### Request Format ```json { "params": { "action": "getOpenInterest" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | --------------------------- | | `params.action` | string | Yes | Must be `"getOpenInterest"` | ### Response #### Success Response ```json { "status": "ok", "response": [ { "symbol": "BTC-USDT", "openInterest": "1250.50", "longOpenInterest": "750.25", "shortOpenInterest": "500.25", "timestamp": 1704067200000 }, { "symbol": "ETH-USDT", "openInterest": "8500.75", "longOpenInterest": "4800.50", "shortOpenInterest": "3700.25", "timestamp": 1704067200000 }, { "symbol": "SOL-USDT", "openInterest": "45000.00", "longOpenInterest": "25000.00", "shortOpenInterest": "20000.00", "timestamp": 1704067200000 } ], "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Failed to retrieve open interest data", "code": "INTERNAL_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Open Interest ```json { "params": { "action": "getOpenInterest" } } ``` ### Open Interest Information * **Active Markets**: Returns open interest for all active/open trading pairs (markets where `isOpen: true`) * **Real-time Data**: Current open interest values * **Capture Time**: `timestamp` (Unix ms) reflects when each market's open interest was last captured by the platform * **String Format**: Open interest returned as strings to preserve precision * **No Filtering**: Always returns data for all active markets ### Use Cases * **Market Analysis**: Understand market participation and liquidity * **Risk Assessment**: Evaluate market depth and volatility * **Trading Strategy**: Incorporate open interest into trading decisions * **Portfolio Management**: Consider market activity in position sizing * **Market Research**: Analyze trading volume and market sentiment ### Open Interest Explained * **Definition**: Total number of outstanding contracts (long + short positions) * **Market Indicator**: Higher open interest often indicates more active trading * **Liquidity Measure**: Markets with high open interest typically have better liquidity * **Trend Analysis**: Changes in open interest can signal market direction * **Risk Metric**: High open interest can indicate potential for large price moves ### Validation Rules * No authentication required * No parameters to validate * Returns data for all active/open markets ### Error Handling Common error scenarios: | Error | Description | | ------------------------------ | ------------------------------------- | | Open interest data unavailable | Failed to retrieve open interest data | | Internal error | Service temporarily unavailable | ### Rate Limiting * **Default**: 1000 requests per minute per IP * **Burst**: Up to 100 requests per second * **Data Updates**: Open interest updates in real-time ### Data Format Notes * **String Format**: All values returned as strings to preserve precision * **No Decimals**: Open interest typically represented as whole numbers * **Symbol Format**: Uses standard trading pair format ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Market configuration * [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) - Funding rates * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current market prices import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; ## Get Orderbook Retrieve order book depth (market depth) for a specific trading pair. ### Request #### Request Format ```json { "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 100 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------ | | `params.action` | string | Yes | Must be `"getOrderbook"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`); max 20 characters; normalized to uppercase | | `params.limit` | integer | No | Number of orders to return per side (default: 500) | #### Valid Limits | Limit | Description | | ------ | ------------------------ | | `5` | Top 5 orders per side | | `10` | Top 10 orders per side | | `20` | Top 20 orders per side | | `50` | Top 50 orders per side | | `100` | Top 100 orders per side | | `500` | Top 500 orders per side | | `1000` | Top 1000 orders per side | ### Response #### Success Response ```json { "status": "ok", "response": { "bids": [ ["45000.00", "1.5"], ["44999.50", "2.0"], ["44999.00", "0.8"] ], "asks": [ ["45001.00", "1.2"], ["45001.50", "3.1"], ["45002.00", "0.9"] ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Depth Response Object | Field | Type | Description | | ------ | ----- | -------------------------------------- | | `bids` | array | Array of bid orders \[price, quantity] | | `asks` | array | Array of ask orders \[price, quantity] | #### Order Array Format Each order in the `bids` and `asks` arrays is represented as: ```json ["price", "quantity"] ``` Where: * `price` (string): Order price * `quantity` (string): Order quantity #### Error Response ```json { "status": "error", "error": { "message": "Invalid limit parameter", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get Top 100 Orders ```json { "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 100 } } ``` #### Get Top 20 Orders ```json { "params": { "action": "getOrderbook", "symbol": "ETH-USDT", "limit": 20 } } ``` ### Market Depth Information * **Real-time Data**: Live order book snapshot * **Bid/Ask Orders**: Separate arrays for buy and sell orders * **Price Priority**: Orders sorted by best price first * **Configurable Depth**: Choose from 5 to 1000 orders per side ### Use Cases * **Market Analysis**: Understand liquidity and market depth * **Trading Decisions**: Analyze order book pressure * **Price Discovery**: See where orders are concentrated * **Liquidity Assessment**: Evaluate market depth for large orders * **Real-time Monitoring**: Track order book changes ### Validation Rules * `symbol` must be a valid trading pair; max 20 characters; normalized to uppercase before lookup * `limit` must be one of the valid limit values: 5, 10, 20, 50, 100, 500, 1000 * No authentication required * Real-time data from matching engine ### Error Handling Common error scenarios: | Error | Description | | ----------------------- | ------------------------------------- | | Symbol required | Missing trading pair symbol | | Invalid limit | Limit not in valid range | | Timeout | Request to matching service timed out | | Market data unavailable | Failed to retrieve order book data | ### Rate Limiting ### Data Format Notes * **Price Precision**: Prices use market-specific decimal precision * **Quantity Precision**: Quantities use market-specific decimal precision * **Order Sorting**: Bids sorted descending (highest first), asks sorted ascending (lowest first) * **Snapshot Consistency**: All data from the same timestamp ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Market configuration * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Current prices * [Get Candles](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) - Historical price data import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; ## getSubAccountIds Returns all subaccount IDs associated with a wallet address. ### Request #### Request Format ```json { "params": { "action": "getSubAccountIds", "walletAddress": "0x742D35CC6634c0532925A3b844BC9E7595F0BEb0" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `params.action` | string | Yes | Must be `getSubAccountIds` | | `params.walletAddress` | string | Yes | Ethereum wallet address to query. Must use EIP-55 checksum casing — non-checksummed addresses return a 400 error with the expected checksummed form. | | `params.includeDelegations` | boolean | No | When `true`, returns owned and delegated subaccount IDs separately. Defaults to `false` (returns owned IDs as a flat array). | ### Response #### Success Response (default) When `includeDelegations` is omitted or `false`, the response is a flat array of owned subaccount IDs: ```json { "status": "ok", "response": [ "1867542890123456789", "1867542890123456790", "1867542890123456791" ], "request_id": "5ccf215d37e3ae6d" } ``` #### Success Response (with `includeDelegations: true`) When `includeDelegations` is `true`, the response is an object with owned and delegated subaccount IDs listed separately: ```json { "status": "ok", "response": { "subAccountIds": [ "1867542890123456789", "1867542890123456790" ], "delegatedSubAccountIds": [ "1867542890123456800" ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields **Default response** (`includeDelegations` omitted or `false`): The response is an array of subaccount ID strings. Each ID is a uint64 represented as a string for precision. **With `includeDelegations: true`**: | Field | Type | Description | | --------------------------------- | --------- | ---------------------------------------------------------------------------- | | `response.subAccountIds` | string\[] | Subaccount IDs owned by the wallet address | | `response.delegatedSubAccountIds` | string\[] | Subaccount IDs for which the wallet address has been granted delegate access | All IDs are uint64 values represented as strings for precision. #### Error Response The following 400 errors are specific to this endpoint: | Error message | HTTP status | Cause | | ------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- | | `walletAddress must use EIP-55 checksum casing; expected {address}` | 400 | Address is valid hex but does not match EIP-55 checksum casing. The expected checksummed form is returned in the message. | | `Invalid wallet address format` | 400 | Address is not a valid Ethereum hex address (must be `0x` + 40 hex characters). | | `Invalid request body` | 400 | `walletAddress` is missing from the request params. | ### Breaking Changes #### EIP-55 checksum enforcement for `walletAddress` `walletAddress` now requires EIP-55 checksum casing. Valid hex addresses that do not match the EIP-55 checksum are rejected with HTTP 400. The error response includes the expected checksummed form. **Before**: Lowercase, uppercase, or arbitrary mixed-case addresses were accepted. **After**: Only EIP-55 checksummed addresses are accepted. ```json // Before (rejected) { "walletAddress": "0x742d35cc6634c0532925a3b844bc9e7595f0beb0" } // After (accepted) { "walletAddress": "0x742D35CC6634c0532925A3b844BC9E7595F0BEb0" } ``` **Migration**: Compute the EIP-55 checksummed address before sending: ```typescript // ethers.js v6 import { getAddress } from 'ethers'; const checksummed = getAddress('0x742d35cc6634c0532925a3b844bc9e7595f0beb0'); // → '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0' // web3.js const checksummed = web3.utils.toChecksumAddress('0x742d35cc6634c0532925a3b844bc9e7595f0beb0'); // → '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0' ``` ### Code Examples #### TypeScript — owned IDs (default) ```typescript const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'getSubAccountIds', walletAddress: '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0' } }) }); const data = await response.json(); console.log('Subaccounts:', data.response); // Array of subaccount IDs ``` #### TypeScript — owned and delegated IDs ```typescript const response = await fetch('https://papi.synthetix.io/v1/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { action: 'getSubAccountIds', walletAddress: '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0', includeDelegations: true } }) }); const data = await response.json(); console.log('Owned subaccounts:', data.response.subAccountIds); console.log('Delegated subaccounts:', data.response.delegatedSubAccountIds); ``` ### Implementation Notes * No authentication required (public endpoint) * Returns all subaccounts ever created for the wallet address * Subaccount IDs are returned as strings to preserve uint64 precision * `includeDelegations: true` triggers a parallel lookup of delegations for the wallet address * **EIP-55 required**: `walletAddress` must use EIP-55 checksum casing. Lowercase, uppercase, or any other non-checksummed form returns a 400 error that includes the expected checksummed address (e.g., `"walletAddress must use EIP-55 checksum casing; expected 0xAbc..."`) ## Info Endpoint Public market data and information endpoints that don't require authentication. ### Overview The info endpoints provide access to public market data, trading information, and system configuration. Note that some streaming data is available through [subscriptions](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions). For the endpoint URL, see [Environments](https://developers.synthetix.io/environments). ### Request Structure All info endpoints wrap inputs inside a `params` object and identify the requested action with the `params.action` field: ```json { "params": { "action": "getMarkets", "symbol": "BTC-USDT", "limit": 100 } } ``` ### Available Endpoints #### Market Information | Method | Action | Description | Purpose | | ------------------------------------------------------------------------ | ---------------- | ---------------------------------------- | ------------------------------------------------------------------ | | [Get Collaterals](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCollaterals) | `getCollaterals` | Retrieves collateral asset configuration | Get supported collateral types, limits, and withdrawal fees | | [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) | `getMarkets` | Retrieves detailed market configuration | Get comprehensive market specifications, trading rules, and limits | #### Market Data | Method | Action | Description | Purpose | | --------------------------------------------------------------------------- | ----------------- | --------------------------------- | -------------------------------------------------- | | [Get Candles](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) | `getCandles` | Retrieves OHLCV price data | Get historical price charts for technical analysis | | [Get Last Trades](https://developers.synthetix.io/developer-resources/api/rest-api/info/getLastTrades) | `getLastTrades` | Retrieves recent trade executions | Get public trade history from the orderbook | | [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) | `getMarketPrices` | Retrieves current market prices | Get real-time price information for all markets | | [Get Orderbook](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) | `getOrderbook` | Retrieves order book snapshot | Get current market depth and liquidity information | #### Market Statistics | Method | Action | Description | Purpose | | ---------------------------------------------------------------------------------------- | ----------------------- | ---------------------------------- | ------------------------------------------------------- | | [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) | `getFundingRate` | Retrieves funding rates | Get current funding rates for perpetual markets | | [Get Funding Rate History](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRateHistory) | `getFundingRateHistory` | Retrieves historical funding rates | Get funding rate history for a market over a time range | | [Get Open Interest](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOpenInterest) | `getOpenInterest` | Retrieves open interest data | Get current open interest for all markets | #### Account & Access | Method | Action | Description | Purpose | | ------------------------------------------------------------------------------ | ------------------ | ------------------------------ | ----------------------------------------------- | | [Get Is Whitelisted](https://developers.synthetix.io/developer-resources/api/rest-api/info/getIsWhitelisted) | `getIsWhitelisted` | Check wallet whitelist status | Verify if wallet has trading permissions | | [Get Sub Account IDs](https://developers.synthetix.io/developer-resources/api/rest-api/info/getSubAccountIds) | `getSubAccountIds` | Get wallet's sub-account IDs | List all sub-accounts for a wallet address | | [Get Mids](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMids) | `getMids` | Get mid prices for all markets | Retrieve average bid-ask prices for all markets | ### Features * No authentication required for public market data * Optimized for frequent data access * Real-time market information and statistics * Consistent response structure across all endpoints * Higher rate limits than authenticated endpoints ### Response All info endpoints return responses in the standard API format: ```json { "status": "ok", "response": [ // Endpoint-specific data ], "requestId": "5ccf215d37e3ae6d", "timestamp": 1704067200000 } ``` ### Error Handling Info endpoints use the same error response format as other API endpoints: ```json { "status": "error", "error": { "message": "Invalid request parameters", "code": "VALIDATION_ERROR" }, "requestId": "5ccf215d37e3ae6d", "timestamp": 1704067200000 } ``` ### Rate Limiting Info endpoints have higher rate limits than trading endpoints: * **Default**: 1000 requests per minute per IP * **Burst**: Up to 100 requests per second * **WebSocket**: Unlimited for real-time subscriptions ### Next Steps * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Start with market configuration * [Get Candles](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) - Access historical price data * [WebSocket API](https://developers.synthetix.io/developer-resources/api/ws-api) - Real-time data streams * [Trading Endpoints](https://developers.synthetix.io/developer-resources/api/rest-api/trade) - Start trading import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Add Delegated Signer Add a delegated signer to a subaccount, allowing another wallet address to perform authorized actions on behalf of the subaccount. This enables secure delegation of trading operations to automated systems, trading bots, or team members without sharing private keys. Multiple delegated signers can be added to a single subaccount. ### Request #### Request Format ```json { "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"] }, "nonce": 1735689600000, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"addDelegatedSigner"` | | `params.subAccountId` | string | Yes | The subaccount ID to add the delegated signer to | | `params.walletAddress` | string | Yes | Ethereum wallet address of the delegated signer (42-character hex format). Note: this request field is `walletAddress`; the EIP-712 signed message uses `delegateAddress` for the same value | | `params.permissions` | string\[] | Yes | Single-item permission array. Use `["session"]` for trading access or `["delegate"]` for trading access plus the ability to create session-level delegations. Older clients may still submit `["trading"]`, which is treated as `["session"]`. | | `params.expiresAt` | integer | No | Optional Unix timestamp (milliseconds) when the delegation expires | | `expiresAfter` | integer | No | Optional request expiration timestamp (milliseconds) | #### EIP-712 Signature Structure The `action` object fields are signed using EIP-712. Note that when `expiresAt` is omitted in the request, it should be set to `0` (not `null`) in the EIP-712 message: ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "expiresAt": null }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | --------------- | --------------- | --------------------------------------------------- | | `subAccountId` | string | The subaccount ID the signer was added to | | `walletAddress` | string | The delegated signer's wallet address | | `permissions` | string\[] | Single-item permission array returned by the server | | `expiresAt` | integer \| null | Expiration timestamp (null if no expiration) | #### Error Response ```json { "status": "error", "error": { "message": "Delegated signer already exists", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Common Error Cases ```json { "status": "error", "error": { "message": "Maximum delegated signers limit reached", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ```json { "status": "error", "error": { "message": "Cannot delegate to self", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Add Session-Level Trading Bot Signer ```json { "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"] }, "nonce": 1735689600000, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Add Delegate-Level Team Member ```json { "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "permissions": ["delegate"] }, "nonce": 1735689600001, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Add Temporary Delegated Signer with Expiration ```json { "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "permissions": ["session"], "expiresAt": 1767225600000 }, "nonce": 1735689600002, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Implementation Notes * **Caller Permissions**: Owners can add `session` or `delegate` signers. `delegate` signers can add `session` signers only * **Multiple Signers**: A subaccount can have multiple delegated signers simultaneously * **Unique Addresses**: Each wallet address can only be delegated once per subaccount * **Permission Levels**: Use `["session"]` for trading access or `["delegate"]` for trading access plus the ability to create session-level delegations * **Immediate Effect**: Delegations take effect immediately upon successful creation * **Optional Expiration**: Delegations can include an expiration timestamp after which they become invalid * **Revocable**: Delegations can be revoked at any time using the `removeDelegatedSigner` endpoint * **Limits**: Platform may enforce maximum number of delegated signers per subaccount ### Security Considerations * **No Self-Delegation**: Accounts cannot delegate to themselves * **Address Validation**: Wallet addresses must be valid Ethereum addresses * **Scoped Access**: `["session"]` signers can trade; `["delegate"]` signers can also create session-level delegations * **Signature Required**: All delegation operations require valid EIP-712 signatures * **Master Control**: The account owner can always manage delegations; `["delegate"]` signers can only create or remove session-level delegations they control ### Delegate Object Structure ### Error Handling ## REST API Authentication ### Authentication Guide **📖 [Authentication Guide](https://developers.synthetix.io/developer-resources/api/authentication)** This guide covers: * **[Overview](https://developers.synthetix.io/developer-resources/api/authentication/overview)** - How authentication works and security model * **[EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing)** - Step-by-step implementation with code examples * **[Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management)** - Best practices for nonce handling * **[Examples](https://developers.synthetix.io/developer-resources/api/authentication/examples)** - Complete SDK implementations in multiple languages * **[Troubleshooting](https://developers.synthetix.io/developer-resources/api/authentication/troubleshooting)** - Debug common authentication issues ### REST EIP-712 Domain All REST trading requests use the shared Synthetix EIP-712 domain: ```json { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" } ``` This domain must be used when signing the action payloads documented in each REST trading method. import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Cancel All Orders Cancel all open orders for specific market(s), or all markets with the wildcard symbol. ### Request #### Request Format ```json { "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["BTC-USDT", "ETH-USDT"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"cancelAllOrders"` | | `params.subAccountId` | string | Yes | SubAccount ID to cancel orders for | | `params.symbols` | array | Yes | Array of symbols to cancel. Use `["*"]` to cancel across all markets. `[]` is rejected. `["*", "ETH-USDT"]` is rejected | #### EIP-712 Type Definition ```typescript const CancelAllOrdersTypes = { CancelAllOrders: [ { name: "subAccountId", type: "uint256" }, { name: "symbols", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` #### Wildcard Rules for `symbols` * `["*"]`: Cancel all open orders across all markets * `["BTC-USDT", "ETH-USDT"]`: Cancel only those markets * `[]`: Rejected * `["*", "ETH-USDT"]`: Rejected (wildcard must be used alone) ### Response #### Success Response ```json { "status": "ok", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "message": "", "symbol": "BTC-USDT" }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "message": "", "symbol": "ETH-USDT" } ], "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Failed to unmarshal cancel all orders request", "code": "INVALID_FORMAT" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Cancel All Markets To cancel all orders across all markets, pass the wildcard symbol as a single-item array: ```json { "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["*"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Cancel Specific Markets To cancel orders for specific markets only: ```json { "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["BTC-USDT", "ETH-USDT"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` ### Cancel Behavior * **Immediate Effect**: Orders are cancelled immediately upon validation * **Margin Release**: Freed margin becomes available instantly * **Partial Fills**: Partially filled orders are cancelled for remaining quantity * **All Order Types**: Cancels limit, market, and trigger orders * **Atomic Operation**: All specified orders are cancelled together ### Implementation Details The API performs the following steps: 1. Retrieves all open orders for the authenticated subaccount 2. Iterates through each order and attempts to cancel it individually 3. Returns an array of cancellation statuses for each processed order 4. Each successful cancellation includes a canonical `order` object (`venueId`, optional `clientId`) **Migration Note**: Use `order.venueId` as canonical. `orderId` remains deprecated for compatibility. The `message` field contains an error description when cancellation fails for a specific order, and is empty on success. Do not treat a non-empty `message` as a success confirmation. ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * `symbols` array must contain valid market symbols or be empty for all markets * `["*"]` cancels all markets and cannot be combined with other symbols * `[]` is invalid * Market symbols must be valid trading pairs * Must be signed by account owner or authorized delegate ### Error Handling Common error scenarios: | Error | Description | | ------------------------- | ----------------------------------- | | Symbols must be non-empty | `symbols` array cannot be empty | | Invalid wildcard usage | `*` must be used alone as `["*"]` | | Invalid market symbol | Market symbol not recognized | | No orders found | No open orders to cancel | | Request expired | `expiresAfter` timestamp has passed | import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Cancel Orders Cancel one or more existing orders by venue order ID or client order ID (CLOID) ### Request #### Request Format Cancel by venue order ID: ```json { "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "orderIds": ["1948058938469519360"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` Cancel by client order ID (CLOID): ```json { "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "clientOrderIds": ["my-order-alpha", "my-order-beta"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` **Note**: `orderIds` and `clientOrderIds` are mutually exclusive. Providing both in the same request returns a validation error. #### Request Parameters | Parameter | Type | Required | Description | | ----------------------- | ------ | ----------- | ------------------------------------------------------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"cancelOrders"` | | `params.subAccountId` | string | Yes | SubAccount ID to cancel orders for | | `params.orderIds` | array | Conditional | Array of venue order IDs to cancel (strings). Mutually exclusive with `clientOrderIds`. | | `params.clientOrderIds` | array | Conditional | Array of client order IDs (CLOIDs) to cancel (strings). Mutually exclusive with `orderIds`. | Exactly one of `orderIds` or `clientOrderIds` must be provided. #### EIP-712 Type Definition **Cancel by venue order ID** (`orderIds` mode) — uses primary type `CancelOrders`: ```typescript const CancelOrdersTypes = { CancelOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orderIds", type: "uint256[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` **Note**: `orderIds` is `uint256[]` in EIP-712, but sent as `string[]` in the JSON request body. **Cancel by client order ID** (`clientOrderIds` mode) — uses primary type `CancelOrdersByCloid`: ```typescript const CancelOrdersByCloidTypes = { CancelOrdersByCloid: [ { name: "subAccountId", type: "uint256" }, { name: "clientOrderIds", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` `clientOrderIds` is `string[]` in both the EIP-712 type and the JSON request body. The EIP-712 primary type must match the cancel mode used. ### Response #### Success Response ```json { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } } ] }, "requestId": "5ccf215d37e3ae6d" } ``` #### Success Response - Multiple Orders ```json { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } }, { "canceled": { "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "id": "1948058938469519361" } } ] }, "requestId": "5ccf215d37e3ae6d" } ``` #### Error Response Request-level errors return an error status. For per-item errors in batch requests, see [Batch Request Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling#batch-request-error-handling). ```json { "status": "error", "error": { "code": "ORDER_NOT_FOUND", "message": "Order not found", "category": "TRADING", "retryable": false }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` #### Partial Success Response When cancelling multiple orders, some may succeed while others fail: ```json { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } }, { "error": "Order not found", "errorCode": "ORDER_NOT_FOUND" } ] }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` ### Code Examples #### Cancel Single Order ```json { "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "orderIds": ["1948058938469519360"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Cancel Multiple Orders ```json { "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "orderIds": ["1948058938469519360", "1948058938469519361"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Cancel by Client Order ID Use `clientOrderIds` to cancel orders using the client-assigned IDs set at order placement. The EIP-712 primary type changes to `CancelOrdersByCloid`. ```json { "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "clientOrderIds": ["my-order-alpha", "my-order-beta"] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` ### Cancel Behavior **Migration Note**: In each status object, use `canceled.order.venueId` as canonical. `canceled.id` is deprecated compatibility output. * Only OPEN/PARTIALLY\_FILLED orders can be cancelled * Filled portions of partially filled orders remain executed * Cancellation is immediate upon validation * Freed margin becomes available instantly ### Order States | State | Can Cancel | | ----------------- | -------------------------- | | OPEN | ✅ Yes | | PARTIALLY\_FILLED | ✅ Yes (remaining quantity) | | FILLED | ❌ No | | CANCELLED | ❌ No | | EXPIRED | ❌ No | ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * Exactly one of `orderIds` or `clientOrderIds` must be provided — providing both returns an error * The provided array must contain at least one entry * When using `orderIds`: each value must be a valid integer (as a string) * When using `clientOrderIds`: values must not have leading or trailing whitespace * Orders must belong to the requesting account * Must be signed by order owner or authorized delegate * The EIP-712 primary type must match the cancel mode: `CancelOrders` for `orderIds`, `CancelOrdersByCloid` for `clientOrderIds` ### Error Handling #### Common Errors | Error Code | Description | | ------------------ | ----------------------------------------------- | | `ORDER_NOT_FOUND` | Order ID does not exist or not owned by user | | `VALIDATION_ERROR` | Order already cancelled or filled | | `INVALID_VALUE` | Empty orderIds array or invalid order ID format | #### Batch Cancel Errors When cancelling multiple orders, each is processed independently. The response includes a status for each order ID in the same sequence as submitted. See [Batch Request Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling#batch-request-error-handling) for details. import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Create Subaccount Create a new trading subaccount under the authenticated wallet address. Subaccounts allow for isolated trading strategies, separate margin management, and organized position tracking within a single master account. ### Request #### Request Format ```json { "params": { "action": "createSubaccount", "subAccountId": "1867542890123456789", "name": "Trading Bot Strategy" }, "nonce": 1735689600000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1735689900 } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ---------------- | -------- | ----------------------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"createSubaccount"` | | `params.subAccountId` | string | Yes | Existing owned subaccount ID used to prove wallet ownership | | `params.name` | string | No | Optional display name for the new subaccount (max 50 chars) | | `nonce` | integer (uint64) | Yes | Positive integer, incrementing nonce | | `signature` | object | Yes | EIP-712 signature for wallet authentication | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | #### Authentication This endpoint requires the wallet owner to sign a `CreateSubaccount` payload that includes an existing owned subaccount ID as proof of wallet ownership. In the request body, send that ownership-proof ID as `params.subAccountId`; in the EIP-712 payload it is signed as `masterSubAccountId`. #### Naming Guidelines * **Optional Parameter**: Name can be omitted for system-generated naming * **Descriptive Names**: Use clear, meaningful names for easy identification when provided * **Character Limit**: Maximum 50 characters when specified * **Default Behavior**: If no name provided, system may generate a default identifier * **Strategy-Based**: Consider naming based on trading strategy or purpose Examples: * "Main Trading" * "DCA Strategy" * "Arbitrage Bot" * "Risk Management" * `""` (empty string) - System handles default naming ### Response #### Success Response (New Account) When a subaccount is first created, `subAccountId`, `subAccountName`, and `accountLimits` are populated. All other fields return zero/null values: ```json { "status": "ok", "response": { "subAccountId": "1867542890123456790", "masterAccountId": null, "subAccountName": "Trading Bot Strategy", "collaterals": null, "crossMarginSummary": { "accountValue": "", "availableMargin": "", "totalUnrealizedPnl": "", "maintenanceMargin": "", "initialMargin": "", "withdrawable": "", "adjustedAccountValue": "", "debt": "" }, "positions": null, "marketPreferences": { "leverages": null }, "feeRates": { "makerFeeRate": "", "takerFeeRate": "", "tierName": "" }, "accountLimits": { "maxBorrowCapacity": "100000", "maxOrdersPerMarket": 50, "maxSubAccounts": 10, "maxTotalOrders": 200 } }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Structure ##### Response Fields | Field | Type | Description | | --------------------------------------- | -------------- | --------------------------------------------------------------------------- | | `subAccountId` | string | **System-generated** unique identifier for the new subaccount (populated) | | `masterAccountId` | string \| null | Master account ID (returns `null`) | | `subAccountName` | string | Display name provided in request (or empty if not specified) (populated) | | `collaterals` | array \| null | Collateral balances (returns `null` for new accounts) | | `collaterals[].adjustedCollateralValue` | string | Adjusted collateral value after applying haircuts | | `collaterals[].calculatedAt` | integer | Unix millisecond timestamp of the collateral price calculation (0 for USDT) | | `collaterals[].collateralValue` | string | Raw collateral value | | `collaterals[].haircutRate` | string | Haircut rate applied to the collateral | | `collaterals[].haircutAdjustment` | string | Calculated haircut adjustment amount | | `collaterals[].pendingWithdraw` | string | Amount pending withdrawal | | `collaterals[].price` | string | Collateral price used for valuation | | `collaterals[].quantity` | string | Total collateral quantity held | | `collaterals[].symbol` | string | Collateral asset symbol | | `collaterals[].withdrawable` | string | Amount available to withdraw | | `crossMarginSummary` | object | Cross-margin account summary (returns empty strings for new accounts) | | `crossMarginSummary.debt` | string | USDT debt after positive unrealized PnL offset | | `positions` | array \| null | Open positions (returns `null` for new accounts) | | `marketPreferences` | object | Market-specific preferences like leverage (returns empty for new accounts) | | `feeRates` | object | Fee rate information (returns empty strings for new accounts) | | `accountLimits` | object | Account limit information | | `accountLimits.maxBorrowCapacity` | string | Maximum borrow capacity in USDT | | `accountLimits.maxOrdersPerMarket` | integer | Maximum open orders allowed per market | | `accountLimits.maxSubAccounts` | integer | Maximum number of subaccounts allowed for the wallet | | `accountLimits.maxTotalOrders` | integer | Maximum total open orders across all markets | #### Initial State * **Populated Fields**: `subAccountId`, `subAccountName`, and `accountLimits` are populated upon creation * **Zero/Null Values**: Other fields are returned with zero or null values since the account has no positions or collaterals yet * **Active Status**: Subaccount is immediately available for trading * **Full Functionality**: All trading features are enabled upon creation * **Additional Details**: Use [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) after deposits/trades to retrieve populated account information including positions and collaterals #### Error Response ```json { "status": "error", "error": { "message": "Failed to create subaccount", "code": "INTERNAL_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Common Error Cases ```json { "status": "error", "error": { "message": "Subaccount limit reached", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ```json { "status": "error", "error": { "message": "Invalid subaccount name", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Create Basic Subaccount ```json { "params": { "action": "createSubaccount", "subAccountId": "1867542890123456789", "name": "Main Trading" }, "nonce": 1735689600000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1735689900 } ``` #### Create Strategy-Specific Subaccount ```json { "params": { "action": "createSubaccount", "subAccountId": "1867542890123456789", "name": "Grid Trading Bot" }, "nonce": 1735689600001, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1735689901 } ``` ### Implementation Notes * Wallet owner signature required, plus an existing owned subaccount ID supplied as `params.subAccountId` for ownership proof * Subaccount name parameter is optional and accepts empty string for system-generated naming * Name length must not exceed 50 characters when provided * Account creation limits may apply per platform configuration * New subaccounts are immediately available for trading operations * Each subaccount inherits master wallet security and authorization settings ### Error Handling import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import CommonRequestParams from "../../../../../snippets/common-request-params.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import NonceManagement from "../../../../../snippets/nonce-management.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Get Balance Updates Retrieve historical deposit, withdrawal, and transfer transactions for a subaccount. This endpoint returns balance changes from on-chain deposits, withdrawals, and internal transfers, excluding funding payments. ### Request #### Request Format ```json { "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "DEPOSIT", "limit": 50, "offset": 0, "startTime": 1704067200000, "endTime": 1704153600000 }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef", "s": "0xabcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getBalanceUpdates"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve balance updates for | | `params.actionFilter` | string | No | Filter by action type: `"DEPOSIT"`, `"WITHDRAWAL"`, `"TRANSFER"`, or comma-separated (e.g. `"DEPOSIT,WITHDRAWAL"`). Defaults to all types | | `params.limit` | integer | No | Maximum number of results to return (default: 50, max: 1000) | | `params.offset` | integer | No | Pagination offset (default: 0, max: 10000) | | `params.startTime` | integer | No | Start of time range as Unix timestamp in milliseconds. Defaults to 7 days before `endTime` (or 7 days ago if neither is set) | | `params.endTime` | integer | No | End of time range as Unix timestamp in milliseconds. Defaults to 7 days after `startTime` (or now if neither is set). Maximum range is 365 days | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Response object containing balance updates (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response ```json { "status": "ok", "response": { "balanceUpdates": [ { "id": "12345", "subAccountId": "1867542890123456789", "action": "DEPOSIT", "status": "success", "amount": "1000.00", "fee": "0.00", "grossAmount": "1000.00", "collateral": "USDT", "timestamp": 1704067200000 }, { "id": "12346", "subAccountId": "1867542890123456789", "action": "WITHDRAWAL", "status": "success", "amount": "-500.00", "fee": "5.00", "grossAmount": "-495.00", "collateral": "USDT", "timestamp": 1704153600000, "destinationAddress": "0xabcdef1234567890abcdef1234567890abcdef12", "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Empty Result ```json { "status": "ok", "response": { "balanceUpdates": [] }, "request_id": "5ccf215d37e3ae6f" } ``` #### Balance Update Object | Field | Type | Description | | -------------------- | ------- | ------------------------------------------------------------------------------------------ | | `id` | string | Unique transaction ID (string for JS BigInt compatibility) | | `subAccountId` | string | Subaccount ID associated with the transaction (string for JS BigInt compatibility) | | `action` | string | Action type: `"DEPOSIT"`, `"WITHDRAWAL"`, or `"TRANSFER"` | | `status` | string | Transaction status: `"pending"`, `"success"`, or `"failed"` | | `amount` | string | Net amount changed (positive for deposits, negative for withdrawals) | | `fee` | string | Fee charged for the transaction | | `grossAmount` | string | Total amount including fee (`amount + fee`) | | `collateral` | string | Collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `timestamp` | integer | Unix timestamp in milliseconds when the transaction was created | | `destinationAddress` | string | Destination address for withdrawals (optional, only for withdrawals) | | `txHash` | string | On-chain transaction hash (optional) | | `fromSubAccountId` | string | Source subaccount ID for internal transfers (optional, string for JS BigInt compatibility) | | `toSubAccountId` | string | Target subaccount ID for internal transfers (optional, string for JS BigInt compatibility) | #### Error Response ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "limit cannot exceed 1000" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Usage Examples #### Get All Balance Updates ```json { "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789" }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Get Deposits Only ```json { "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "DEPOSIT" }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Get Withdrawals Only ```json { "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "WITHDRAWAL" }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Paginated Request ```json { "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "limit": 100, "offset": 100 }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" }, ], }; const message = { subAccountId: "1867542890123456789", action: "getBalanceUpdates", expiresAfter: 0, }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Returns deposit, withdrawal, and internal transfer transactions (excludes funding payments) * Results are ordered by timestamp (most recent first) * Default limit is 50 results per request, maximum is 1000 * Pagination offset maximum is 10,000 * Use pagination with `limit` and `offset` for large result sets * When `startTime` and `endTime` are both omitted, the default time window is the last 7 days * When only one time bound is supplied, the other is automatically extended by 7 days (end time is capped at now) * Maximum allowed time range is 365 days; requests spanning a longer period are rejected * For funding payment history, use [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) * Authentication requires signature by account owner ### Error Handling Common error scenarios: | Error Code | Message | Description | | ------------------ | ------------------------------------------------------------------------- | ------------------------------- | | `VALIDATION_ERROR` | `subAccountId is required` | No subaccount ID was provided | | `VALIDATION_ERROR` | `limit cannot exceed 1000` | Limit parameter exceeds maximum | | `VALIDATION_ERROR` | `limit must be non-negative` | Negative limit value provided | | `VALIDATION_ERROR` | `offset must be non-negative` | Negative offset value provided | | `VALIDATION_ERROR` | `offset cannot exceed 10000` | Offset exceeds maximum of 10000 | | `VALIDATION_ERROR` | `invalid action filter, available filters: DEPOSIT, WITHDRAWAL, TRANSFER` | Invalid action filter value | | `INTERNAL_ERROR` | `Failed to retrieve balance updates` | Server error retrieving data | ### Related Endpoints * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - View current account balances and margin status * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) - Funding payment history (separate from deposits/withdrawals) import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Delegated Signers Retrieve a list of all delegated signers for a specific subaccount. This endpoint provides visibility into which wallet addresses have been granted permissions to act on behalf of the subaccount, along with their permission levels and delegation details. ### Request #### Request Format ```json { "params": { "action": "getDelegatedSigners", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ----------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"getDelegatedSigners"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve delegated signers for | ### Response #### Success Response ```json { "status": "ok", "response": { "delegatedSigners": [ { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "1867542890123456789", "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "permissions": ["delegate"], "expiresAt": 1767225600000 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ------------------ | ----- | --------------------------------- | | `delegatedSigners` | array | Array of delegated signer objects | #### Empty Response ```json { "status": "ok", "response": { "delegatedSigners": [] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Delegated Signers ```json { "params": { "action": "getDelegatedSigners", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Response with Multiple Signers ```json { "status": "ok", "response": { "delegatedSigners": [ { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "1867542890123456789", "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "permissions": ["delegate"], "expiresAt": null }, { "subAccountId": "1867542890123456789", "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "permissions": ["session"], "expiresAt": 1798761600000 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getDelegatedSigners", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * **Access Control**: Both master account owners and delegated signers can view the delegation list * **Real-time Data**: Returns current active delegations only * **Expiration Handling**: Expired delegations are automatically excluded from results * **No Pagination**: All delegated signers are returned in a single response ### Use Cases * **Team Management**: View all team members with access to a trading account * **Security Audit**: Regular review of who has access to perform operations * **Access Control UI**: Build interfaces showing current permissions * **Bot Management**: Track which automated systems have trading access * **Compliance**: Maintain records of account access for regulatory purposes ### Error Handling import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Delegations For Delegate Retrieve a list of all delegations granted to the authenticated wallet address. This endpoint allows a delegate wallet to discover which accounts have delegated permissions to it, enabling frontends to detect when a connected wallet is a delegate and allow switching to the delegator's account. ### Request #### Request Format ```json { "params": { "action": "getDelegationsForDelegate", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"getDelegationsForDelegate"` | | `params.subAccountId` | string | Yes | Subaccount ID included in the signed `SubAccountAction` payload | | `params.owningAddress` | string | No | Optional Ethereum address to query on behalf of. When provided and different from the signing wallet, the caller must have `trading` permission for that address | | `expiresAfter` | integer | No | Unix timestamp (seconds) when this request expires | | `signature` | object | Yes | EIP-712 signature object | \::::info Authentication The delegate address is determined from the wallet that signed the request. By default, this returns delegations for the signing wallet. If you provide `owningAddress`, the caller must have `trading` permission for that address. \:::: ### Response #### Success Response ```json { "status": "ok", "response": { "delegatedAccounts": [ { "subAccountId": "1867542890123456789", "ownerAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "accountName": "Main Trading Account", "accountValue": "12500.50", "adjustedAccountValue": "11875.47", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "2987654321098765432", "ownerAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "accountName": "Secondary Account", "accountValue": "4300.00", "adjustedAccountValue": "4085.00", "permissions": ["delegate"], "expiresAt": 1767225600000 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ------------------- | ----- | ---------------------------------- | | `delegatedAccounts` | array | Array of delegated account objects | #### Delegated Account Object | Field | Type | Description | | ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `subAccountId` | string | Subaccount ID this delegation applies to | | `ownerAddress` | string | Ethereum wallet address of the account owner who granted the delegation | | `accountName` | string | Human-readable name of the delegated account | | `accountValue` | string | Total account value of the delegating subaccount in USDT including unrealized P\&L | | `adjustedAccountValue` | string | Haircut-adjusted collateral value plus unrealized P\&L, in USDT | | `permissions` | string\[] | Array of permission levels granted. Current responses typically return `["session"]` or `["delegate"]`; legacy records may still show `["trading"]` | | `expiresAt` | integer \| null | Unix timestamp (milliseconds) when the delegation expires. `null` indicates no expiration | #### Empty Response ```json { "status": "ok", "response": { "delegatedAccounts": [] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Authentication failed", "code": "UNAUTHORIZED" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Delegations for Connected Wallet ```json { "params": { "action": "getDelegationsForDelegate", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Response with Multiple Delegated Accounts ```json { "status": "ok", "response": { "delegatedAccounts": [ { "subAccountId": "1867542890123456789", "ownerAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "accountName": "Main Trading Account", "accountValue": "12500.50", "adjustedAccountValue": "11875.47", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "2987654321098765432", "ownerAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "accountName": "Secondary Account", "accountValue": "4300.00", "adjustedAccountValue": "4085.00", "permissions": ["delegate"], "expiresAt": null }, { "subAccountId": "3456789012345678901", "ownerAddress": "0xA1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0", "accountName": "Team Account", "accountValue": "980.25", "adjustedAccountValue": "931.24", "permissions": ["session"], "expiresAt": 1798761600000 } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt("1867542890123456789"), action: "getDelegationsForDelegate", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * **Delegate-Centric Query**: Unlike `getDelegatedSigners` which queries by subaccount, this endpoint returns all accounts that have delegated permissions to the authenticated wallet * **Access Control**: Returns delegations for the signing wallet by default, or for `owningAddress` when the caller has `trading` permission for that address * **Real-time Data**: Returns current active delegations only * **Expiration Handling**: Expired delegations are automatically excluded from results * **No Pagination**: All delegations are returned in a single response * **Owner Information**: Includes the `ownerAddress`, `accountName`, `accountValue`, and `adjustedAccountValue` to help identify which accounts have granted delegation. `adjustedAccountValue` reflects haircut-adjusted collateral plus unrealized P\&L * **Permission Values**: New session-level grants are stored and returned as `session`; legacy records may still appear as `trading` ### Use Cases * **Wallet Detection**: Frontend can detect when a connected wallet is a delegate and offer account switching * **Account Switching UI**: Build interfaces allowing delegates to switch between delegated accounts * **Multi-Account Management**: Delegates managing multiple accounts can see all their delegations in one place * **Access Verification**: Confirm which accounts a delegate has permission to trade on * **Team Dashboard**: Team members can see which accounts they have access to ### Error Handling #### `owningAddress`-Specific Errors | Status | Code | When it happens | | ------ | ------------------ | --------------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | `owningAddress` is not a valid hex Ethereum address | | 403 | `FORBIDDEN` | Caller does not have `trading` permission for the specified `owningAddress` | ### Next Steps * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegatedSigners) - Query delegations by subaccount * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/addDelegatedSigner) - Add new delegation * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/removeDelegatedSigner) - Revoke delegation * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Account details * [WebSocket Alternative](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegationsForDelegate) - WebSocket API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Funding Payments Retrieve detailed funding payment history and statistics for the authenticated subaccount. This endpoint provides user-specific funding data including payments received, paid, and net funding across all positions. ### Request #### Request Format ```json { "params": { "action": "getFundingPayments", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getFundingPayments"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve funding data for | | `params.symbol` | string | No | Filter by specific trading pair | | `params.startTime` | number | No | Start timestamp for filtering (ms). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `params.endTime` | number | No | End timestamp for filtering (ms). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. | | `params.limit` | number | No | Maximum number of records to return. Default: 100. Maximum: 1000. | ### Response #### Success Response ```json { "status": "ok", "response": { "summary": { "totalFundingReceived": "125.75000000", "totalFundingPaid": "89.25000000", "netFunding": "36.50000000", "totalPayments": "247", "averagePaymentSize": "0.87044534" }, "fundingHistory": [ { "paymentId": "fp_1958787130134106112", "symbol": "BTC-USDT", "positionSize": "2.50000000", "fundingRate": "0.00001250", "payment": "-1.12500000", "timestamp": 1735689600000, "paymentTime": 1735689600000, "fundingTimestamp": 1735689600000, "fundingTime": 1735689600000 }, { "paymentId": "fp_1958787130134106111", "symbol": "ETH-USDT", "positionSize": "-5.00000000", "fundingRate": "-0.00002100", "payment": "3.15000000", "timestamp": 1735661200000, "paymentTime": 1735661200000, "fundingTimestamp": 1735661200000, "fundingTime": 1735661200000 } ] }, "request_id": "a3f7c2d1e9b8x67k" } ``` #### Response Fields ##### Summary Object | Field | Type | Description | | ---------------------- | ------ | ------------------------------------- | | `totalFundingReceived` | string | Total funding payments received | | `totalFundingPaid` | string | Total funding payments paid | | `netFunding` | string | Net funding (received - paid) | | `totalPayments` | string | Total number of funding payments | | `averagePaymentSize` | string | Average payment size (absolute value) | ##### Funding History Object | Field | Type | Description | | ------------------ | ------ | ---------------------------------------------------------------------- | | `paymentId` | string | Unique identifier for the funding payment | | `symbol` | string | Trading pair symbol | | `positionSize` | string | Position size at funding time (signed) | | `fundingRate` | string | Funding rate applied (1-hour rate) | | `payment` | string | Funding payment amount (negative = paid out) | | `timestamp` | number | **DEPRECATED** - Use `paymentTime` instead. When payment was processed | | `paymentTime` | number | When payment was processed (replaces `timestamp`) | | `fundingTimestamp` | number | **DEPRECATED** - Use `fundingTime` instead. Funding period timestamp | | `fundingTime` | number | Funding period timestamp (replaces `fundingTimestamp`) | #### Payment Calculation **Formula**: `Payment = Position Size × Funding Rate × Mark Price` * **Positive payment**: Funding received * **Negative payment**: Funding paid * **Long positions**: Pay when funding rate is positive * **Short positions**: Receive when funding rate is positive ### Authentication This endpoint requires EIP-712 signature authentication. #### Request Signing #### Example Authentication ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const value = { subAccountId: "1867542890123456789", action: "getFundingPayments", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, value); ``` ### Error Responses #### Endpoint-Specific Errors | HTTP Status | Error Code | Message | Solution | | ----------- | ------------------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | subAccountId is required | Provide a valid authenticated subaccount | | 400 | `VALIDATION_ERROR` | invalid symbol | Use a valid symbol (e.g., `BTC-USDT`) from getMarkets | | 400 | `VALIDATION_ERROR` | startTime must be less than endTime | Ensure `startTime` is strictly less than `endTime` | | 400 | `VALIDATION_ERROR` | startTime must be a valid timestamp | Use a timestamp within the last 30 days and not in the future | | 400 | `VALIDATION_ERROR` | startTime older than 30 days | `startTime` cannot be more than 30 days in the past | | 400 | `VALIDATION_ERROR` | endTime older than 30 days | `endTime` is older than 30 days and no `startTime` was provided — provide a `startTime` within the last 30 days | | 400 | `VALIDATION_ERROR` | limit must not exceed 1000 | Reduce the limit to 1000 or below | | 400 | `INVALID_FORMAT` | Invalid request body | Check request structure and field types | ### Use Cases #### Funding Performance Analysis ```javascript // Analyze funding performance over time const fundingData = await getFundingPayments({ subAccountId: "1867542890123456789", startTime: Date.now() - (30 * 24 * 60 * 60 * 1000) // Last 30 days }); const netFunding = parseFloat(fundingData.result.summary.netFunding); console.log(`Net funding last 30 days: ${netFunding} USDT`); ``` #### Position-Specific Funding ```javascript // Get funding for specific market const btcFunding = await getFundingPayments({ subAccountId: "1867542890123456789", symbol: "BTC-USDT" }); ``` #### Funding Rate Impact ```javascript // Calculate funding rate impact on strategy const fundingHistory = fundingData.result.fundingHistory; const highRatePayments = fundingHistory.filter( payment => Math.abs(parseFloat(payment.fundingRate)) > 0.01 ); ``` ### Related Endpoints * [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) - Current market funding rates * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Current positions * Get Performance History - Overall performance metrics import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import CommonRequestParams from "../../../../../snippets/common-request-params.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import NonceManagement from "../../../../../snippets/nonce-management.mdx"; import OrderObject from "../../../../../snippets/order-object.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Get Open Orders Retrieve all currently open orders for the authenticated subaccount. This endpoint returns only active orders (not filled or cancelled), with optional filtering by symbol. Note: `side`, `type`, and `status` params are accepted in the request but are not forwarded to the backend and have no filtering effect. ### Request #### Request Format ```json { "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "limit": 50, "offset": 0 }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef", "s": "0xabcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ---------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getOpenOrders"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve orders for | | `params.symbol` | string | No | Filter orders by specific market symbol (e.g., "BTC-USDT") | | `params.side` | string | No | Accepted but currently has no filtering effect | | `params.type` | string | No | Accepted but currently has no filtering effect | | `params.status` | string | No | Accepted but currently has no filtering effect | | `params.limit` | integer | No | Maximum number of orders to return (default: 50) | | `params.offset` | integer | No | Number of orders to skip for pagination (default: 0) | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | ----------------------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | array | Array of open order objects matching filter criteria (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response ```json { "status": "ok", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "symbol": "BTC-USDT", "side": "buy", "type": "LIMIT", "quantity": "0.1", "price": "45000.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": false, "closePosition": false, "createdTime": 1755846234000, "updatedTime": 1755846234000, "filledQuantity": "0.0", "takeProfitOrder": { "venueId": "1958787130134106115", "clientId": "cli-1958787130134106115" }, "takeProfitOrderId": "1958787130134106115", "stopLossOrder": { "venueId": "1958787130134106116", "clientId": "cli-1958787130134106116" }, "stopLossOrderId": "1958787130134106116" }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "symbol": "ETH-USDT", "side": "sell", "type": "limit", "quantity": "2.0", "price": "2800.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": true, "closePosition": false, "createdTime": 1755846235000, "updatedTime": 1755846235000, "filledQuantity": "0.5" }, { "order": { "venueId": "1958787130134106120", "clientId": "cli-1958787130134106120" }, "orderId": "1958787130134106120", "symbol": "BTC-USDT", "side": "buy", "type": "limit", "quantity": "1.0", "price": "45000.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": false, "closePosition": false, "createdTime": 1755846236000, "updatedTime": 1755846240000, "filledQuantity": "0.5", "expiresAt": 1755850000000 } ], "request_id": "5ccf215d37e3ae6d" } ``` #### Empty Result ```json { "status": "ok", "response": [], "request_id": "5ccf215d37e3ae6f" } ``` #### Order Object Fields | Field | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `order` | object | Canonical order identifier object | | `order.venueId` | string | Canonical venue-generated order identifier | | `order.clientId` | string | Optional client-provided order identifier | | `orderId` | string | Deprecated legacy order identifier (venue ID string) | | `symbol` | string | Trading pair symbol (e.g., "BTC-USDT") | | `side` | string | Order side: `"buy"` or `"sell"` | | `type` | string | Order type (e.g., `"LIMIT"`, `"MARKET"`) | | `quantity` | string | Total order quantity | | `price` | string | Order price (empty for market orders) | | `triggerPrice` | string | Trigger price for conditional orders (populated for stop market, stop limit, take profit limit, take profit market; omitted for non-conditional orders) | | `triggerPriceType` | string | Reference price used to evaluate the trigger: `"mark_price"` or `"last_price"` | | `timeInForce` | string | Time in force: `"GTC"`, `"IOC"`, or `"FOK"` | | `reduceOnly` | boolean | Whether order can only reduce position | | `postOnly` | boolean | Whether order must be maker (no immediate match) | | `closePosition` | boolean | Whether order closes entire position | | `createdTime` | integer | Order creation timestamp (Unix milliseconds) | | `updatedTime` | integer | Last update timestamp (Unix milliseconds) | | `filledQuantity` | string | Quantity that has been filled | | `takeProfitOrder` | object | Canonical linked take-profit order identifier object | | `takeProfitOrderId` | string | Deprecated linked take-profit venue ID | | `stopLossOrder` | object | Canonical linked stop-loss order identifier object | | `stopLossOrderId` | string | Deprecated linked stop-loss venue ID | | `expiresAt` | integer | Order expiration timestamp (Unix milliseconds); omitted when the order has no explicit expiry | **Migration Note**: Use `order.venueId` as the canonical order ID. Legacy `orderId`, `takeProfitOrderId`, and `stopLossOrderId` fields are deprecated compatibility fields. **Note on TP/SL Fields**: When orders are placed with `normalTpsl` or `positionTpsl` grouping, the entry order will contain `takeProfitOrder` / `stopLossOrder` linking to the associated TP/SL trigger orders. Deprecated `takeProfitOrderId` / `stopLossOrderId` are still returned for compatibility. #### Error Response ```json { "status": "error", "error": { "code": "INVALID_FORMAT", "message": "Invalid request body" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Usage Examples #### Get All Open Orders ```json { "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789" }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Get Open Orders for Specific Market ```json { "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT" }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Get Open Buy Orders with Pagination ```json { "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "side": "buy", "limit": 100, "offset": 0 }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getOpenOrders", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Returns only active orders (placed, partially filled) * Does not include filled, cancelled, or rejected orders * For historical orders with time ranges and status filters, use [`getOrdersHistory`](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOrdersHistory) * Pagination uses limit/offset approach (default limit: 50) * Orders are returned in creation order (newest first) * Authentication requires signature by account owner or authorized delegate * Linked TP/SL orders: use `takeProfitOrder.venueId` and `stopLossOrder.venueId` as canonical IDs (`takeProfitOrderId` / `stopLossOrderId` remain deprecated compatibility fields) ### Error Handling Common error scenarios: | Error | Description | | ---------------------- | ----------------------------------- | | Invalid request format | Request payload could not be parsed | | Internal error | Failed to retrieve orders | | Subaccount not found | Specified subaccount does not exist | | Invalid symbol | Specified symbol is not recognized | | Invalid pagination | Limit or offset out of valid range | ### Comparison with getOrdersHistory | Feature | getOpenOrders | getOrdersHistory | | -------------------- | ----------------------------- | ------------------------------------- | | **Purpose** | Currently open orders only | All orders with comprehensive history | | **Time Filtering** | Not supported | fromTime/toTime range | | **Status Filtering** | Param accepted, no effect | Multiple status filters | | **Default Scope** | Active orders only | All orders (any status) | | **Use Case** | Real-time order management | Historical analysis and reporting | | **Sorting** | Creation order (newest first) | Configurable sort fields/order | import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import CommonRequestParams from "../../../../../snippets/common-request-params.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import NonceManagement from "../../../../../snippets/nonce-management.mdx"; import OrderObject from "../../../../../snippets/order-object.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Get Orders History Retrieve orders history for the authenticated subaccount with comprehensive filtering, sorting, and pagination capabilities, providing access to orders in any status with flexible query options. ### Request #### Request Format ```json { "params": { "action": "getOrderHistory", "subAccountId": "1867542890123456789", "status": ["placed", "filled"], "startTime": 1730160000000, "endTime": 1730764800000, "limit": 100 }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef", "s": "0xabcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getOrderHistory"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve orders for | | `params.status` | string\[] | No | Filter by order status: `["started", "placed", "partially filled", "filled", "cancelling", "cancelled", "rejected", "modifying", "modified", "unknown"]` | | `params.symbol` | string | No | Filter by specific trading pair symbol (e.g., "BTC-USDT") | | `params.side` | string | No | Filter by order side: `"buy"` or `"sell"` | | `params.type` | string | No | Filter by order type (e.g., `"LIMIT"`, `"MARKET"`) | | `params.startTime` | integer | No | Start timestamp in milliseconds (inclusive). Max 7-day range. Preferred over `fromTime` | | `params.endTime` | integer | No | End timestamp in milliseconds (inclusive). Max 7-day range. Preferred over `toTime` | | `params.fromTime` | integer | No | Start timestamp in milliseconds (inclusive). Deprecated — use `startTime` instead | | `params.toTime` | integer | No | End timestamp in milliseconds (inclusive). Deprecated — use `endTime` instead | | `params.limit` | integer | No | Maximum number of orders to return (no default specified, max: 1000) | | `params.clientOrderId` | string | No | Filter by client-provided order ID. Must not contain leading or trailing whitespace | #### Filter Combinations The endpoint supports powerful filter combinations: * **All Orders**: Omit `status` to get orders in any status * **Historical Orders**: `"status": ["filled", "cancelled"]` with time range * **Recent Activity**: Use `startTime` without `endTime` for orders since a specific time * **Symbol-Specific**: Combine `symbol` with any other filters * **Client Order Lookup**: Use `clientOrderId` to retrieve a specific order by its client-assigned ID ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------ | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | array | Array of order objects matching filter criteria (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response Examples ##### Multiple Orders with Various Statuses ```json { "status": "ok", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "symbol": "BTC-USDT", "side": "buy", "type": "LIMIT", "status": "FILLED", "quantity": "0.1", "price": "45000.00", "timeInForce": "GTC", "createdTime": 1755846234000, "updateTime": 1755846290000, "filledQuantity": "0.1", "filledPrice": "44998.50", "triggeredByLiquidation": false, "reduceOnly": false, "postOnly": false }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "symbol": "ETH-USDT", "side": "sell", "type": "LIMIT", "status": "PARTIALLY_FILLED", "quantity": "2.0", "price": "2800.00", "timeInForce": "GTC", "reduceOnly": false, "postOnly": false, "createdTime": 1755846235000, "updateTime": 1755846291000, "filledQuantity": "0.5", "filledPrice": "2801.25", "triggeredByLiquidation": false }, { "order": { "venueId": "1958787130134106114", "clientId": "cli-1958787130134106114" }, "orderId": "1958787130134106114", "symbol": "BTC-USDT", "side": "buy", "type": "LIMIT", "status": "NEW", "timeInForce": "GTC", "quantity": "0.05", "price": "44000.00", "reduceOnly": false, "postOnly": false, "createdTime": 1755846236000, "updateTime": 1755846236000, "filledQuantity": "0.0", "filledPrice": "0.0", "triggeredByLiquidation": false } ], "request_id": "5ccf215d37e3ae6d" } ``` ##### Empty Result Set ```json { "status": "ok", "response": [], "request_id": "5ccf215d37e3ae6f" } ``` #### Order Object **Additional field on order history responses:** | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------------------------------------ | | `cancelReason` | string | Optional. Human-readable reason the order was cancelled. Omitted when not applicable | **Migration Note**: Use `order.venueId` as the canonical order identifier. `orderId` is deprecated and returned for compatibility. **Note on TP/SL auto-cancel**: When a take-profit or stop-loss order in a linked `normalTpsl` pair fills, the platform automatically cancels the sibling order **without creating a history entry**. The filled leg appears with `status: "Filled"` as expected; the auto-cancelled sibling will not appear in the results. Do not rely on an auto-cancelled sibling row when reconciling linked TP/SL pairs. #### Error Response ```json { "status": "error", "error": { "message": "Invalid time range: fromTime must be less than or equal to toTime", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Advanced Filtering #### Time-based Queries ```json // Orders from last 24 hours "startTime": Date.now() - 86400000 // Orders in specific date range "startTime": 1704067200000, "endTime": 1704153600000 // Orders since specific time (no end) "startTime": 1704067200000 ``` `fromTime` and `toTime` are deprecated aliases for `startTime` and `endTime` respectively and are still accepted. #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getOrderHistory", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Authentication requires signature by account owner or authorized delegate * **Time range cannot exceed 7 days** (604,800,000 milliseconds) * Time range filtering supports inclusive bounds with `startTime` ≤ `endTime` validation * Use `startTime`/`endTime` for time range filtering; `fromTime`/`toTime` are deprecated aliases * Pagination: Use `limit` parameter (no default specified, max: 1000) * Status filtering accepts multiple values * `clientOrderId` filter must not contain leading or trailing whitespace ### Error Handling Common error scenarios: | Error | Description | | ---------------------- | ------------------------------------------------------------------- | | Invalid request format | Request payload could not be parsed | | Internal error | Failed to retrieve orders | | Invalid time range | `startTime` is greater than `endTime`, or time range exceeds 7 days | | Invalid clientOrderId | `clientOrderId` contains leading or trailing whitespace | | Invalid status | Specified status is not recognized | | Invalid symbol | Specified symbol is not recognized | | Invalid sort field | Specified sortBy field is invalid | | Invalid pagination | Limit or offset out of valid range | | Subaccount not found | Specified subaccount does not exist | import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Get Performance History Retrieve comprehensive performance analytics including historical account value, PnL trends, and trading volume for a subaccount over a specified period. Optimized for charting and performance analysis. ### Request #### Request Format ```json { "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "week" }, "expiresAfter": 1735689600, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Container for action-specific parameters | | `params.action` | string | Yes | Must be `"getPerformanceHistory"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve performance history for | | `params.period` | string | No | Time window for the performance history. One of `day` (default), `week`, `month`, `threeMonth`, `ytd`, `allTime`. | If `params.period` is omitted, the service returns `day` performance history by default. ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "123456789", "period": "day", "performanceHistory": { "history": [ { "sampledAt": 1736947200000, "accountValue": "10000.50", "pnl": "12.34" }, { "sampledAt": 1736950800000, "accountValue": "10020.10", "pnl": "15.67" } ], "volume": "250000.00" } }, "request_id": "abcd1234" } ``` #### Response Structure ##### Performance History Object | Field | Type | Description | | ------------------------ | ------ | ----------------------------------------------------------------------------------------- | | `history` | array | Ordered list of performance points for the requested period | | `history[]` | object | Individual datapoint containing timestamp and performance values | | `history[].sampledAt` | number | Unix timestamp in milliseconds | | `history[].accountValue` | string | Account value at the timestamp | | `history[].pnl` | string | PnL at the timestamp, derived from account equity deltas with external cash flows removed | | `volume` | string | Total volume traded during the requested period | The response also echoes the `subAccountId` and the `period` that was requested. `history[].pnl` is calculated from account equity movement with deposits, withdrawals, and transfers removed from the result. If you previously reconciled this field against realized trade PnL plus funding plus a latest unrealized snapshot, historical values may differ. #### Error Response ```json { "status": "error", "error": { "message": "Failed to retrieve performance history", "code": "INTERNAL_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get Performance History ```json { "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "month" }, "expiresAfter": 1735689600, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getPerformanceHistory", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Performance Information * **Historical Analytics**: Account value and PnL tracking for a selected timeframe (day, week, month, threeMonth, YTD, all-time) * **Volume Tracking**: Trading activity metrics for performance analysis over the requested period * **Time Series Data**: Ideal for charting and portfolio performance visualization * **Performance Focus**: Dedicated to performance analytics, separate from account balances and margin data * **USDT Denomination**: All monetary values are denominated in USDT for consistency * **Optimized for Analytics**: Structured for performance dashboards and reporting tools ### Use Cases * **Performance Dashboards**: Build comprehensive performance tracking interfaces * **PnL Analysis**: Track profit and loss trends over different time periods * **Trading Analytics**: Analyze trading volume and activity patterns * **Portfolio Reporting**: Generate performance reports for users * **Charting Applications**: Power performance charts and visualizations * **Trend Analysis**: Identify performance patterns and trading behavior ### Validation Rules * `expiresAfter` is optional; if provided, request must be used before this timestamp * Must be signed by account owner or authorized delegate * Subaccount ID must be valid and accessible #### EIP-712 Domain Separator All signature-based authentication uses the following domain separator: ```json { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" } ``` ### Error Handling Common error scenarios: | Error | Description | | ------------------------ | --------------------------------------- | | Invalid request format | Request payload could not be parsed | | Internal error | Failed to retrieve performance data | | Subaccount not found | Specified subaccount does not exist | | Insufficient permissions | Account lacks access to this subaccount | ### Related Endpoints * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Account information and balances * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Current position details * [Get Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOrdersHistory) - Trading history * [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - Trade execution details ### Data Format Notes * **Timestamps**: Unix timestamp in milliseconds * **Time Series**: Array of objects with `sampledAt`, `accountValue`, and `pnl`, ordered chronologically * **Numeric Precision**: All numeric values returned as strings to preserve precision * **Volume (`volume`)**: Represents total volume traded during the requested time period import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Position History Retrieve closed position history for an authenticated subaccount with symbol and time-range filtering plus pagination. ### Request #### Request Format ```json { "params": { "action": "getPositionHistory", "subaccountId": "123456789", "symbol": "BTC-USDT", "startTime": 1769364177000, "endTime": 1769450577000, "limit": 50, "offset": 0 }, "signature": { "v": 27, "r": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "s": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getPositionHistory"` | | `params.subaccountId` | string | Yes | Subaccount ID in request payload. The authenticated selected account is enforced server-side for access control | | `params.symbol` | string | No | Filter by trading pair (for example `"BTC-USDT"`) | | `params.startTime` | integer | No | Start timestamp in milliseconds (inclusive). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. Maximum range between `startTime` and `endTime` is 30 days. | | `params.endTime` | integer | No | End timestamp in milliseconds (inclusive). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. Maximum range between `startTime` and `endTime` is 30 days. | | `params.limit` | integer | No | Number of records to return (default: 100, max: 1000) | | `params.offset` | integer | No | Number of records to skip (default: 0, max: 10000) | | `signature` | object | Yes | EIP-712 signature for the request | This read endpoint does not require nonce management in its request flow. ### Response #### Success Response ```json { "status": "ok", "response": { "positions": [ { "positionId": "2015847946616049664", "symbol": "BTC-USDT", "side": "long", "entryPrice": "95000", "quantity": "0.001", "closePrice": "96000", "closeReason": "close", "leverage": 10, "realizedPnl": "1", "accumulatedFees": "0.1", "netFunding": "0", "closedAt": 1769450577774, "createdAt": 1769450577000, "tradeId": "123" } ], "hasMore": false }, "requestId": "abcd1234", "request_id": "abcd1234", "timestamp": 1769450577774 } ``` #### Response Fields | Field | Type | Description | | -------------------- | ------- | -------------------------------------------------------------- | | `status` | string | `"ok"` on success, `"error"` on failure | | `response` | object | Container for result data | | `response.positions` | array | Closed position records for the requested filters | | `response.hasMore` | boolean | Whether additional records are available for pagination | | `requestId` | string | Request tracking identifier | | `request_id` | string | Legacy request tracking identifier (same value as `requestId`) | | `timestamp` | integer | Server timestamp in milliseconds | #### Position History Object | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------ | | `positionId` | string | Unique position identifier | | `symbol` | string | Trading pair symbol | | `side` | string | Position side (`"long"` or `"short"`) | | `entryPrice` | string | Average entry price | | `quantity` | string | Position size | | `closePrice` | string | Execution price of closure | | `closeReason` | string | Position close reason (for example `"close"`) | | `leverage` | integer | Position leverage at the time of close. Omitted when not set | | `realizedPnl` | string | Realized PnL for this closed position | | `accumulatedFees` | string | Total fees paid for the position lifecycle | | `netFunding` | string | Net funding paid/received | | `closedAt` | integer | Position close timestamp in milliseconds | | `createdAt` | integer | Position creation timestamp in milliseconds | | `tradeId` | string | Associated trade identifier | #### Error Response ```json { "status": "error", "error": { "category": "REQUEST", "message": "Invalid time range", "code": "VALIDATION_ERROR", "retryable": false }, "requestId": "abcd1234", "request_id": "abcd1234", "timestamp": 1769450577774 } ``` ### EIP-712 Signature ```javascript const domain = { name: 'Synthetix', version: '1', chainId: 1, verifyingContract: '0x0000000000000000000000000000000000000000' }; const types = { SubAccountAction: [ { name: 'subAccountId', type: 'uint256' }, { name: 'action', type: 'string' }, { name: 'expiresAfter', type: 'uint256' } ] }; const message = { subAccountId: '123456789', action: 'getPositionHistory', expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Error Handling | Error | Description | | ---------------------------- | --------------------------------------------------------------------------------------------------------------- | | Invalid request format | Request payload is malformed | | Invalid subaccount | Subaccount does not exist or is not accessible | | Invalid time range | `startTime` is greater than `endTime` | | Time range exceeds maximum | `startTime` and `endTime` are more than 30 days apart | | startTime older than 30 days | `startTime` cannot be more than 30 days in the past | | endTime older than 30 days | `endTime` is older than 30 days and no `startTime` was provided — provide a `startTime` within the last 30 days | | Offset exceeds maximum | `offset` is greater than 10000 | | Invalid pagination | `limit` or `offset` is outside accepted bounds | `symbol` is a filter field, but this endpoint does not document dedicated symbol-validation errors in its handler path. import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import PositionObject from '../../../../../snippets/position-object.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Positions Retrieve detailed information about positions for the authenticated subaccount with time range filtering and pagination. This unified endpoint supports both current and historical position data. ### Request #### Request Format ```json { "params": { "action": "getPositions", "subAccountId": "1867542890123456789", "status": "open", "symbol": "BTC-USDT", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 50, "offset": 0, "sortBy": "createdAt", "sortOrder": "desc" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getPositions"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve positions for | | `params.status` | string | No | Filter by position status: `"open"`, `"close"`, or `"update"` | | `params.symbol` | string | No | Filter by trading symbol (e.g., `"BTC-USDT"`) | | `params.startTime` | integer | No | Start time in Unix milliseconds (inclusive) | | `params.endTime` | integer | No | End time in Unix milliseconds (inclusive) | | `params.fromTime` | integer | No | **Deprecated** — use `startTime` instead | | `params.toTime` | integer | No | **Deprecated** — use `endTime` instead | | `params.limit` | integer | No | Maximum number of positions to return (default: 50) | | `params.offset` | integer | No | Number of positions to skip for pagination (default: 0) | | `params.sortBy` | string | No | Field to sort results by | | `params.sortOrder` | string | No | Sort direction: `"asc"` or `"desc"` | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | --------------------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | array | Array of position objects matching filter criteria (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Client-provided request identifier echoed back from the request | #### Success Response Examples ##### Multiple Positions (Open and Closed) ```json { "status": "ok", "response": [ { "adlBucket": 1, "positionId": "pos_12345", "subAccountId": "1867542890123456789", "symbol": "ETH-USDT", "side": "long", "entryPrice": "3000.00", "quantity": "0.5000", "realizedPnl": "150.00", "unrealizedPnl": "0.00", "usedMargin": "0.00", "maintenanceMargin": "0.00", "liquidationPrice": "0.00", "status": "close", "netFunding": "8.50", "takeProfitOrders": [], "takeProfitOrderIds": [], "stopLossOrders": [], "stopLossOrderIds": [], "updatedAt": 1735689600000, "createdAt": 1735686000000 }, { "adlBucket": 4, "positionId": "pos_12346", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "short", "entryPrice": "42000.00", "quantity": "0.1000", "realizedPnl": "0.00", "unrealizedPnl": "-12.50", "usedMargin": "840.00", "maintenanceMargin": "420.00", "liquidationPrice": "45000.00", "status": "open", "netFunding": "8.50", "takeProfitOrders": [], "takeProfitOrderIds": [], "stopLossOrders": [ { "venueId": "2026771048053084162", "clientId": "cli-sl_002" } ], "stopLossOrderIds": ["2026771048053084162"], "updatedAt": 1735689600000, "createdAt": 1735680000000 } ], "request_id": "5ccf215d37e3ae6d" } ``` ##### Open Positions Only ```json { "status": "ok", "response": [ { "adlBucket": 2, "positionId": "pos_12347", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "long", "entryPrice": "43500.00", "quantity": "0.2000", "realizedPnl": "0.00", "unrealizedPnl": "200.00", "usedMargin": "1740.00", "maintenanceMargin": "870.00", "liquidationPrice": "41000.00", "status": "open", "netFunding": "8.50", "takeProfitOrders": [ { "venueId": "2026771048053084163", "clientId": "cli-tp_001" } ], "takeProfitOrderIds": ["2026771048053084163"], "stopLossOrders": [ { "venueId": "2026771048053084164", "clientId": "cli-sl_003" } ], "stopLossOrderIds": ["2026771048053084164"], "updatedAt": 1735689700000, "createdAt": 1735689000000 } ], "request_id": "5ccf215d37e3ae6e" } ``` ##### Empty Result Set ```json { "status": "ok", "response": [], "request_id": "5ccf215d37e3ae6f" } ``` #### Position Object **Migration Note**: Use `takeProfitOrders[*].venueId` and `stopLossOrders[*].venueId` as canonical order references. `takeProfitOrderIds` and `stopLossOrderIds` are deprecated compatibility fields. #### Position Status Types | Status | Description | Characteristics | | -------- | ---------------------- | ---------------------------------------------------------- | | `open` | Active position | Has unrealized PnL, margin requirements, liquidation price | | `close` | Closed position | Only realized PnL, no margin requirements | | `update` | Position being updated | Transitional state during modifications | #### Position Calculations * `realizedPnl`: Profit/loss from closed portions of the position * `unrealizedPnl`: Current profit/loss based on mark price (open positions only) * `usedMargin`: Margin currently allocated to this position * `maintenanceMargin`: Minimum margin for position maintenance * `liquidationPrice`: Price at which position would be liquidated (open positions only) * `entryPrice`: Volume-weighted average price of all fills that opened the position * `netFunding`: Net funding payments received/paid for this position * `adlBucket`: Auto-Deleveraging priority bucket (1–5). Positions in bucket 5 are the first to be auto-deleveraged in a liquidation event. Buckets are assigned per market per side based on PnL ratio and effective leverage, recalculated periodically. New positions default to bucket 1 until the next ranking cycle. #### Error Response ```json { "status": "error", "error": { "message": "Failed to get positions", "code": "INTERNAL_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Get All Open Positions ```json { "params": { "action": "getPositions", "subAccountId": "1867542890123456789", "status": "open", "limit": 50 }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Position Information * **Real-time Data**: Open position data is updated in real-time with current mark prices and PnL * **Risk Metrics**: Includes liquidation prices, margin requirements, and position-level risk assessment * **Order Links**: References to associated take profit and stop loss orders * **Detailed Margins**: Both used margin and maintenance margin requirements * **USDT Denomination**: All monetary values are denominated in USDT for consistency * **Performance Tracking**: Comprehensive PnL, volume, and fee tracking per position ### Code Example #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getPositions", expiresAfter: Math.floor(Date.now() / 1000) + 60 // 1 min from now }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Authentication requires signature by account owner or authorized delegate * Subaccount ID must be valid and accessible to requesting wallet * Pagination: Use `limit` and `offset` parameters for result pagination * Returns all positions for the subaccount in current implementation ### Error Handling Common error scenarios: import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Rate Limits Retrieve current rate limit usage and capacity for the authenticated user. This endpoint provides real-time information about API request consumption, helping developers monitor usage and avoid hitting rate limits. ### Request #### Request Format ```json { "params": { "action": "getRateLimits", "subAccountId": "1867542890123456789", "user": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef", "s": "0xabcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | --------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getRateLimits"` | | `params.subAccountId` | string | Yes | SubAccount ID for authentication | | `params.user` | string | Yes | Wallet address to check rate limits for | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | --------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Rate limit data (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response ```json { "status": "ok", "response": { "requestsUsed": 45, "requestsCap": 1200 }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------- | | `requestsUsed` | integer | Total requests used in the current rate limit window | | `requestsCap` | integer | Maximum total requests allowed in the current window | #### Error Response ```json { "status": "error", "error": { "code": "INVALID_FORMAT", "message": "Invalid request body" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Usage Examples #### Check Rate Limit Status ```json { "params": { "action": "getRateLimits", "subAccountId": "1867542890123456789", "user": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getRateLimits", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Returns rate limit usage for the authenticated user * Authentication requires signature by account owner or authorized delegate * Uses `SubAccountAction` EIP-712 type (no nonce required, only optional `expiresAfter`) ### Rate Limit Information This endpoint itself is subject to rate limiting. For detailed rate limit policies, see [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits). ### Error Handling Common error scenarios: | Error | Description | | ---------------------- | ------------------------------------ | | Invalid request format | Request payload could not be parsed | | Authentication error | Invalid or missing EIP-712 signature | ### Related Endpoints * [Rate Limits Overview](https://developers.synthetix.io/developer-resources/api/rate-limits) — Comprehensive rate limiting documentation * [WebSocket Alternative](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getRateLimits) — WebSocket API equivalent import CollateralObject from '../../../../../snippets/collateral-object.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import CrossMarginSummaryObject from '../../../../../snippets/cross-margin-summary-object.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubaccountObject from '../../../../../snippets/subaccount-object.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Subaccount This endpoint retrieves detailed information for a specific subaccount, providing essential account metadata, collateral information, and cross margin summary. For detailed position information, use the `getPositions` endpoint. For performance analytics, use the `getPerformanceHistory` endpoint. ### Request #### Request Format ```json { "params": { "action": "getSubAccount", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Field | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------ | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getSubAccount"` | | `params.subAccountId` | string | Yes | The ID of the subaccount to retrieve | ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "masterAccountId": "1867542890123456788", "subAccountName": "Trading Account 1", "collaterals": [ { "symbol": "USDC", "quantity": "1000.00000000", "withdrawable": "1000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "1000.00000000", "collateralValue": "1000.00000000", "haircutRate": "0", "haircutAdjustment": "0", "price": "1.00000000", "calculatedAt": 0 }, { "symbol": "ETH", "quantity": "0.5000", "withdrawable": "0.5000", "pendingWithdraw": "0.0000", "adjustedCollateralValue": "875.00000000", "collateralValue": "1000.00000000", "haircutRate": "0.125", "haircutAdjustment": "125.00000000", "price": "2000.00000000", "calculatedAt": 1735689900000 } ], "crossMarginSummary": { "accountValue": "6750.50", "availableMargin": "2415.00", "totalUnrealizedPnl": "75.00", "maintenanceMargin": "1207.50", "initialMargin": "2415.00", "withdrawable": "4335.50", "adjustedAccountValue": "6750.50", "debt": "0.00" }, "positions": [], "marketPreferences": { "leverages": { "BTC-USDT": 20, "ETH-USDT": 10 } }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "Regular User" }, "accountLimits": { "maxBorrowCapacity": "100000.00", "maxOrdersPerMarket": 200, "maxSubAccounts": 10, "maxTotalOrders": 1000 } }, "request_id": "5ccf215d37e3ae6d" } ``` #### Subaccount Object #### Collateral Object #### Cross Margin Summary Object #### Error Response ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Subaccount Information * **Account Access**: Must have access rights to the specified subaccount * **Delegated Access**: Works with delegated signer permissions * **Account Metadata**: Provides complete account information for the specified subaccount * **Collateral Overview**: Shows all assets held in the subaccount * **Master Account Relationships**: Identifies the master account hierarchy if applicable * **Margin Overview**: Provides comprehensive margin and balance data across all positions * **Complementary Data**: Use with `getPositions` for position details and `getPerformanceHistory` for performance analytics ### Code Examples #### Get Specific Subaccount ```json { "params": { "action": "getSubAccount", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` **Response includes:** * Detailed subaccount metadata * Collateral balances * Cross margin summary * Market preferences * Position information (if applicable) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getSubAccount", expiresAfter: 0 // Optional, use 0 for no expiration }; const signature = await signer._signTypedData(domain, types, message); ``` ### Related Endpoints * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Get positions for a subaccount * [Get Performance History](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPerformanceHistory) - View trading performance * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - Track balance changes import CollateralObject from '../../../../../snippets/collateral-object.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import CrossMarginSummaryObject from '../../../../../snippets/cross-margin-summary-object.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubaccountObject from '../../../../../snippets/subaccount-object.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Get Subaccounts Retrieve all subaccounts under the same master account as the authenticated subaccount. Works for both account owners and delegated signers — any subaccount that can authenticate will receive the full list of sibling subaccounts, each including collateral balances, cross margin summary, positions, market preferences, fee rates, and delegated signers. This is a more comprehensive alternative to `getSubAccount` that returns multiple subaccounts with delegation information included. ### Request #### Request Format ```json { "params": { "action": "getSubAccounts", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Field | Type | Required | Description | | --------------------- | ------ | -------- | -------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getSubAccounts"` | | `params.subAccountId` | string | Yes | The ID of a subaccount owned by the authenticated wallet | ### Response #### Success Response ```json { "status": "ok", "response": { "subAccounts": [ { "subAccountId": "1867542890123456789", "masterAccountId": "1867542890123456788", "subAccountName": "Trading Account 1", "collaterals": [ { "symbol": "USDC", "quantity": "1000.00000000", "withdrawable": "1000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "1000.00", "collateralValue": "1000.00", "haircutRate": "0", "haircutAdjustment": "0.00", "price": "1.0000", "calculatedAt": 0 }, { "symbol": "ETH", "quantity": "0.5000", "withdrawable": "0.5000", "pendingWithdraw": "0.0000", "adjustedCollateralValue": "975.00", "collateralValue": "1000.00", "haircutRate": "0.025", "haircutAdjustment": "25.00", "price": "2000.00", "calculatedAt": 1735689600000 } ], "crossMarginSummary": { "accountValue": "6750.50", "availableMargin": "2415.00", "totalUnrealizedPnl": "75.00", "maintenanceMargin": "1207.50", "initialMargin": "2415.00", "withdrawable": "4335.50", "adjustedAccountValue": "6750.50", "debt": "0.00" }, "positions": [], "marketPreferences": { "leverages": { "BTC-USDT": 20, "ETH-USDT": 10 } }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "Regular User" }, "accountLimits": { "maxBorrowCapacity": "10000.00", "maxOrdersPerMarket": 10, "maxSubAccounts": 10, "maxTotalOrders": 100 }, "delegatedSigners": [ { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["trading"], "expiresAt": null, "addedBy": "0x1234567890abcdef1234567890abcdef12345678" }, { "subAccountId": "1867542890123456789", "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "permissions": ["trading"], "expiresAt": 1767225600000 } ] }, { "subAccountId": "1867542890123456790", "masterAccountId": "1867542890123456788", "subAccountName": "Trading Account 2", "collaterals": [ { "symbol": "USDC", "quantity": "5000.00000000", "withdrawable": "5000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "5000.00", "collateralValue": "5000.00", "haircutRate": "0", "haircutAdjustment": "0.00", "price": "1.0000", "calculatedAt": 0 } ], "crossMarginSummary": { "accountValue": "5000.00", "availableMargin": "5000.00", "totalUnrealizedPnl": "0.00", "maintenanceMargin": "0.00", "initialMargin": "0.00", "withdrawable": "5000.00", "adjustedAccountValue": "5000.00", "debt": "0.00" }, "positions": [], "marketPreferences": { "leverages": {} }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "Regular User" }, "accountLimits": { "maxBorrowCapacity": "10000.00", "maxOrdersPerMarket": 10, "maxSubAccounts": 10, "maxTotalOrders": 100 }, "delegatedSigners": [] } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ------------- | ----- | --------------------------- | | `subAccounts` | array | Array of subaccount objects | #### Subaccount Object Each subaccount in the `subAccounts` array includes the standard subaccount fields plus a `delegatedSigners` array: #### Additional Field | Field | Type | Description | | ------------------ | ----- | ----------------------------------------------------- | | `delegatedSigners` | array | Array of delegated signer objects for this subaccount | #### Collateral Object #### Cross Margin Summary Object #### Delegate Object #### Error Response ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Subaccount Information * **Account Access**: Must have access rights to the specified subaccount (owner or delegated signer) * **Delegated Access**: Works with delegated signer permissions — delegates see all sibling subaccounts under the same master * **Multiple Accounts**: Returns all subaccounts under the same master account, not just the authenticated subaccount * **Delegation Visibility**: Includes delegated signers for each subaccount, unlike `getSubAccount` * **Complementary Data**: Use with `getPositions` for detailed position information and `getPerformanceHistory` for performance analytics ### Code Examples #### Get All Subaccounts ```json { "params": { "action": "getSubAccounts", "subAccountId": "1867542890123456789" }, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` **Response includes for each subaccount:** * Detailed subaccount metadata * Collateral balances * Cross margin summary * Market preferences * Position information * Fee rates * Delegated signers with permissions and expiration #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getSubAccounts", expiresAfter: 0 // Optional, use 0 for no expiration }; const signature = await signer._signTypedData(domain, types, message); ``` ### Comparison with getSubAccount | Feature | `getSubAccount` | `getSubAccounts` | | ----------------- | ----------------------- | ------------------------------------------ | | Returns | Single subaccount | All subaccounts for the wallet | | Delegated Signers | Not included | Included for each subaccount | | Use Case | Specific account lookup | Full account overview with delegation info | ### Related Endpoints * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Get a single subaccount * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegatedSigners) - Get delegated signers for a single subaccount * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Get positions for a subaccount * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - Track balance changes import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; import TradeObject from '../../../../../snippets/trade-object.mdx'; ## Get Trades Retrieve trade execution history (fills) from Synthetix's orderbook. Each trade represents a filled order or partial fill that has been executed. Returns trades for the specified subaccount. ### Request #### Request Format ```json { "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "limit": 100, "offset": 0, "startTime": 1730678400000, "endTime": 1730764800000 }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Request Parameters #### Action Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getTrades"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve trades for | | `params.symbol` | string | No | Filter trades by market symbol (e.g., `"BTC-USDT"`) | | `params.orderId` | string | No | Filter trades by venue order ID (only returns trades for this order) | | `params.limit` | integer | No | Maximum number of trades to return (default: 100, max: 1000) | | `params.offset` | integer | No | Number of trades to skip for pagination (default: 0) | | `params.startTime` | integer | No | Start timestamp in milliseconds (inclusive). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `params.endTime` | integer | No | End timestamp in milliseconds (inclusive). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. | #### Common Request Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------- | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | ### Response #### Response Structure | Field | Type | Description | | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Response object containing trades array and pagination info (omitted when `status` is `"error"`) | | `response.trades` | array | Array of trade objects matching filter criteria | | `response.trades[].orderType` | string | Order type that produced this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `response.hasMore` | boolean | Whether more trades exist beyond current result | | `response.total` | integer | Total number of trades matching the query | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response Examples ##### Multiple Trades with Various Details ```json { "status": "ok", "response": { "trades": [ { "tradeId": "123456789", "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "orderId": "1948058938469519360", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "limit", "price": "50000.50", "quantity": "0.1", "realizedPnl": "125.50", "fee": "5.00", "feeRate": "0.001", "markPrice": "50025.00", "entryPrice": "49500.00", "timestamp": 1704067200500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456790", "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "orderId": "1948058938469519361", "symbol": "ETH-USDT", "side": "sell", "direction": "close long", "orderType": "market", "price": "2999.25", "quantity": "1.5", "realizedPnl": "-75.25", "fee": "4.50", "feeRate": "0.001", "markPrice": "3000.00", "entryPrice": "2950.00", "timestamp": 1704067201000, "maker": true, "reduceOnly": true, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456791", "order": { "venueId": "1948058938469519362", "clientId": "cli-1948058938469519362" }, "orderId": "1948058938469519362", "symbol": "BTC-USDT", "side": "sell", "direction": "open short", "orderType": "stopMarket", "price": "49500.00", "quantity": "0.05", "realizedPnl": "0.00", "fee": "2.48", "feeRate": "0.001", "markPrice": "49450.00", "entryPrice": "50000.00", "timestamp": 1704067201500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": true, "postOnly": false } ], "hasMore": false, "total": 3 }, "request_id": "5ccf215d37e3ae6d" } ``` ##### Single Symbol Filter ```json { "status": "ok", "response": { "trades": [ { "tradeId": "123456792", "order": { "venueId": "1948058938469519363", "clientId": "cli-1948058938469519363" }, "orderId": "1948058938469519363", "symbol": "BTC-USDT", "side": "buy", "direction": "close short", "orderType": "limit", "price": "50250.00", "quantity": "0.2", "realizedPnl": "50.00", "fee": "10.05", "feeRate": "0.001", "markPrice": "50275.00", "entryPrice": "50100.00", "timestamp": 1704067202000, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false } ], "hasMore": true, "total": 150 }, "request_id": "5ccf215d37e3ae6e" } ``` ##### Empty Result Set ```json { "status": "ok", "response": { "trades": [], "hasMore": false, "total": 0 }, "request_id": "5ccf215d37e3ae6f" } ``` #### Trade Object #### Trade Field Explanations | Field | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `tradeId` | Unique identifier for this specific trade execution | | `order.venueId` | Canonical venue-generated order ID for this trade | | `order.clientId` | Optional client-provided order ID for this trade | | `orderId` | Deprecated legacy order ID field | | `orderType` | Type of order that generated this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `price` | Actual execution price at which this trade was filled (not the mark price or entry price) | | `quantity` | Actual filled quantity for this trade execution | | `realizedPnl` | Profit/loss realized from this trade | | `fee` | Trading fee charged for this execution | | `feeRate` | Fee rate applied (maker/taker rate) | | `markPrice` | Mark price at time of trade execution (used for unrealized P\&L and liquidations) | | `entryPrice` | Position's average entry price at time of trade (weighted average of all entries) | | `maker` | Whether this trade provided liquidity (maker) or took liquidity (taker) | | `reduceOnly` | Whether this trade reduced an existing position | | `triggeredByLiquidation` | Whether this trade was part of a liquidation | | `postOnly` | Whether this order was post-only (maker-only) | #### Error Response ```json { "status": "error", "error": { "message": "Invalid time range: startTime must be less than or equal to endTime", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Examples #### Get Recent Trades (Own Account) ```json { "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "limit": 50 }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Filter by Symbol ```json { "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "symbol": "ETH-USDT", "limit": 100 }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Filter by Order ID ```json { "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360" }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Sorting Trades are returned in descending order by timestamp (newest first). #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getTrades", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Subaccount parameter required for trade retrieval scope * **30-day lookback cap:** `startTime` cannot be more than 30 days in the past. When `startTime` is omitted, it defaults to `now − 30 days`. If `endTime` is older than 30 days without an explicit `startTime`, the request is rejected. * **Time range cannot exceed 30 days** (2,592,000,000 milliseconds) * Pagination supports up to 1000 trades per request (default: 100) * Time filtering uses inclusive millisecond timestamp bounds * Symbol filtering reduces response size for multi-market accounts * Order ID filtering via `orderId` returns only trades for a specific order (use `order.venueId` from trade history as the filter value) * Trade identification uses unique `tradeId` with canonical order linking via `order.venueId` (`orderId` field on the trade object is deprecated) * The `direction` field indicates the position effect (e.g., `"open long"`, `"close long"`, `"open short"`, `"close short"`) ### Trade Types #### Regular Trades Standard trades from limit or market orders: * Contains standard execution information * May be partial fills (group by `order.venueId` to see all fills for the same order) #### Liquidation Trades Forced trades due to insufficient margin: * Identified by `triggeredByLiquidation: true` * Otherwise contains the same fields as regular trades #### Maker vs Taker * **Maker**: Order added liquidity to the orderbook (`maker: true`) * Typically receives fee rebate or reduced fees * **Taker**: Order removed liquidity from the orderbook (`maker: false`) * Typically pays higher fees ### Error Handling #### Common Errors | Error | Description | Solution | | --------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Invalid time range` | Start time is after end time, time range exceeds 30 days, or `startTime` is more than 30 days in the past | Ensure `startTime` \< `endTime`, range ≤ 30 days, and `startTime` is within the last 30 days | | `Invalid limit` | Limit exceeds maximum | Use limit ≤ 1000 | | `Invalid offset` | Negative offset value | Use offset ≥ 0 | | `Invalid symbol` | Market symbol not recognized | Check available markets | | `Invalid orderId` | orderId is non-numeric, zero, or exceeds the maximum allowed value | Use a positive numeric string matching a valid venue order ID | | `Authentication failed` | Invalid signature | Verify signature generation | | `Unauthorized access` | No permission for specified subAccountId | Ensure you have access to the specified subaccount | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### Rate Limiting * Each request counts as 1 against rate limits * Large queries (high limits) may have higher weight * Consider caching trade history for frequently accessed data * See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for details ### Next Steps * [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - Create new orders * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/cancelOrders) - Cancel existing orders * [WebSocket Trading](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Real-time trade updates import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; import TradeObject from '../../../../../snippets/trade-object.mdx'; ## Get Trades For Position Retrieve trade execution history (fills) for a specific position within a subaccount. Unlike [`getTrades`](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades), which returns all trades for a subaccount, this endpoint filters trades to those associated with a single position identified by `positionId`. ### Request #### Request Format ```json { "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42", "limit": 100, "offset": 0 }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Request Parameters #### Action Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------------ | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getTradesForPosition"` | | `params.subAccountId` | string | Yes | SubAccount ID that owns the position | | `params.positionId` | string | Yes | Position ID to retrieve trades for (numeric string) | | `params.limit` | integer | No | Maximum number of trades to return (default: 100, max: 1000) | | `params.offset` | integer | No | Number of trades to skip for pagination (default: 0) | #### Common Request Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------- | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | ### Response #### Response Structure | Field | Type | Description | | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Response object containing trades array and pagination info (omitted when `status` is `"error"`) | | `response.trades` | array | Array of trade objects for the specified position | | `response.trades[].orderType` | string | Order type that produced this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `response.hasMore` | boolean | Whether more trades exist beyond current result | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response Examples ##### Trades for a Position ```json { "status": "ok", "response": { "trades": [ { "tradeId": "123456789", "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "orderId": "1948058938469519360", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "limit", "price": "50000.50", "quantity": "0.1", "realizedPnl": "0.00", "fee": "5.00", "feeRate": "0.001", "markPrice": "50025.00", "entryPrice": "50000.50", "timestamp": 1704067200500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456790", "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "orderId": "1948058938469519361", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "market", "price": "50100.00", "quantity": "0.05", "realizedPnl": "0.00", "fee": "2.51", "feeRate": "0.001", "markPrice": "50110.00", "entryPrice": "50033.67", "timestamp": 1704067201000, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false } ], "hasMore": false }, "request_id": "5ccf215d37e3ae6d" } ``` ##### Empty Result Set ```json { "status": "ok", "response": { "trades": [], "hasMore": false }, "request_id": "5ccf215d37e3ae6f" } ``` #### Trade Object #### Trade Field Explanations | Field | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `tradeId` | Unique identifier for this specific trade execution | | `order.venueId` | Canonical venue-generated order ID for this trade | | `order.clientId` | Optional client-provided order ID for this trade | | `orderId` | Deprecated legacy order ID field | | `orderType` | Type of order that generated this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `price` | Actual execution price at which this trade was filled (not the mark price or entry price) | | `quantity` | Actual filled quantity for this trade execution | | `realizedPnl` | Profit/loss realized from this trade | | `fee` | Trading fee charged for this execution | | `feeRate` | Fee rate applied (maker/taker rate) | | `markPrice` | Mark price at time of trade execution (used for unrealized P\&L and liquidations) | | `entryPrice` | Position's average entry price at time of trade (weighted average of all entries) | | `maker` | Whether this trade provided liquidity (maker) or took liquidity (taker) | | `reduceOnly` | Whether this trade reduced an existing position | | `triggeredByLiquidation` | Whether this trade was part of a liquidation | | `postOnly` | Whether this order was post-only (maker-only) | #### Error Response ```json { "status": "error", "error": { "message": "positionId is required", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Examples #### Get All Trades for a Position ```json { "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42" }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Paginated Request ```json { "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42", "limit": 50, "offset": 100 }, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getTradesForPosition", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Requires a valid subaccount ID (authenticated via EIP-712 signature) * `positionId` is required and must be a valid numeric string * Pagination supports up to 1000 trades per request (default: 100) * Offset must be non-negative * The `direction` field indicates the position effect (e.g., `"open long"`, `"close long"`, `"open short"`, `"close short"`) * Unlike `getTrades`, this endpoint does not support `symbol`, `orderId`, `startTime`, or `endTime` filters — it returns all trades for the specified position * The response does not include a `total` count field; use `hasMore` for pagination ### Error Handling #### Common Errors | Error | Description | Solution | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `positionId is required` | Missing position ID | Provide `positionId` in request params | | `positionId must be a valid numeric value` | Non-numeric position ID | Use a numeric string for `positionId` | | `subaccountId is required` | Missing subaccount | Include `subAccountId` in request params | | `Invalid limit` | Limit exceeds maximum or is negative | Use limit between 0 and 1000 | | `Invalid offset` | Negative offset value | Use offset >= 0 | | `Authentication failed` | Invalid signature | Verify signature generation | | `Unauthorized access` | No permission for specified subAccountId | Ensure you have access to the specified subaccount | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### Rate Limiting * Each request counts as 1 against rate limits * See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for details ### Next Steps * [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - Retrieve all trades for a subaccount * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Query current positions * [Get Position History](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositionHistory) - Query closed position history * [WebSocket Trading](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Real-time trade updates import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import CommonRequestParams from "../../../../../snippets/common-request-params.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import NonceManagement from "../../../../../snippets/nonce-management.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Get Transfers Retrieve transfer history for a subaccount, including collateral transfers between subaccounts. Supports filtering by collateral symbol and time range, with pagination. ### Request #### Request Format ```json { "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "symbol": "USDT", "limit": 50, "offset": 0, "startTime": 1740220800000, "endTime": 1740307200000 }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef", "s": "0xabcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600abcdef19480589384695193600" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getTransfers"` | | `params.subAccountId` | string | Yes | SubAccount ID to retrieve transfers for | | `params.symbol` | string | No | Filter by collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `params.limit` | integer | No | Maximum number of results to return (default: 50, max: 1000) | | `params.offset` | integer | No | Pagination offset (default: 0) | | `params.startTime` | number | No | Start timestamp in milliseconds. Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `params.endTime` | number | No | End timestamp in milliseconds. If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. Maximum range between `startTime` and `endTime`: 30 days. | ### Response #### Response Structure | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Response object containing transfers (omitted when `status` is `"error"`) | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | #### Success Response ```json { "status": "ok", "response": { "transfers": [ { "transferId": "12345", "from": "2011391943438766080", "to": "3022502054549877191", "symbol": "USDT", "amount": "100", "transferType": "COLLATERAL_TRANSFER", "status": "success", "timestamp": 1740307200000 }, { "transferId": "12346", "from": "2011391943438766080", "to": "3022502054549877191", "symbol": "WETH", "amount": "0.5", "transferType": "COLLATERAL_TRANSFER", "status": "success", "timestamp": 1740220800000 } ], "total": 2 }, "request_id": "5ccf215d37e3ae6d" } ``` #### Empty Result ```json { "status": "ok", "response": { "transfers": [], "total": 0 }, "request_id": "5ccf215d37e3ae6f" } ``` #### Transfer Object | Field | Type | Description | | -------------- | ------ | --------------------------------------------------------------- | | `transferId` | string | Unique transfer identifier (string for JS BigInt compatibility) | | `from` | string | Source subaccount ID | | `to` | string | Destination subaccount ID | | `symbol` | string | Collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `amount` | string | Transfer amount as a decimal string | | `transferType` | string | Transfer type (e.g., `"COLLATERAL_TRANSFER"`) | | `status` | string | Transfer status (e.g., `"success"`, `"pending"`, `"failed"`) | | `errorMessage` | string | Error details when the transfer failed (omitted when empty) | | `timestamp` | number | Unix timestamp in milliseconds when the transfer occurred | #### Error Response ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "subAccountId is required" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Usage Examples #### Get All Transfers ```json { "params": { "action": "getTransfers", "subAccountId": "1867542890123456789" }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Filter by Symbol ```json { "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "symbol": "USDT" }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Filter by Time Range ```json { "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "startTime": 1740220800000, "endTime": 1740307200000 }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### Paginated Request ```json { "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "limit": 100, "offset": 100 }, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" }, ], }; const message = { subAccountId: "1867542890123456789", action: "getTransfers", expiresAfter: 0, }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * Returns transfer history including collateral transfers between subaccounts * Results are ordered by timestamp (most recent first) * Default limit is 50 results per request, maximum is 1000 * Maximum lookback is 30 days — `startTime` cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. If `endTime` is older than 30 days without an explicit `startTime`, the request is rejected. * Maximum time range is 30 days when using `startTime` and `endTime` * Use pagination with `limit` and `offset` for large result sets * Authentication requires signature by account owner using `SubAccountAction` EIP-712 type (no nonce required) ### Error Handling Common error scenarios: | Error Code | Message | Description | | ------------------ | ----------------------------- | ------------------------------------- | | `VALIDATION_ERROR` | `subAccountId is required` | No subaccount ID was provided | | `VALIDATION_ERROR` | `limit cannot exceed 1000` | Limit parameter exceeds maximum | | `VALIDATION_ERROR` | `limit must be non-negative` | Negative limit value provided | | `VALIDATION_ERROR` | `offset must be non-negative` | Negative offset value provided | | `VALIDATION_ERROR` | `invalid request parameters` | Invalid time range (exceeds 30 days) | | `INVALID_FORMAT` | `invalid request format` | Malformed request body | | `INTERNAL_ERROR` | `failed to get transfers` | Server error retrieving transfer data | ### Related Endpoints * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - Deposit and withdrawal transaction history * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - View current account balances and margin status ## Trade Endpoints All trade methods go through the below endpoints: * **REST**: `POST https://papi.synthetix.io/v1/trade` ### Authentication All trade methods are signed using EIP-712. Each successful trade request will contain: * A piece of structured data that includes the sender address * A signature of the hash of that structured data, signed by the sender ### Subaccounts Subaccounts must also follow the signing process defined above. Subaccount delegate private keys are assigned signing authority from the master account. ### Expires After Some trade actions support an optional field `expiresAfter` which is a Unix timestamp in seconds after which the action will be rejected. ### Available Methods #### Order Management | Method | Type | Description | Purpose | | --------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | [Cancel Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/cancelOrders) | `order` | Cancels one or more existing orders | Remove one or more orders from the orderbook | | [Modify Order](https://developers.synthetix.io/developer-resources/api/rest-api/trade/modifyOrder) | `order` | Modifies a single existing order's parameters | Update individual order price, size, or other parameters without canceling and replacing | | [Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) | `order` | Places one or more orders (market, limit, or conditional) on the orderbook | Execute single or multiple buy/sell orders for perpetual contracts and spot trading with efficient batch operations | #### Account Management | Method | Type | Description | Purpose | | -------------------------------------------------------------------------------------- | --------- | -------------------------------------- | ------------------------------------------------------------------------ | | [Create Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/createSubaccount) | `account` | Creates a new trading subaccount | Organize trading strategies with isolated accounts and margin management | | [Update Leverage](https://developers.synthetix.io/developer-resources/api/rest-api/trade/updateLeverage) | `account` | Adjusts leverage for trading positions | Modify risk exposure by changing position leverage | | [Update Subaccount Name](https://developers.synthetix.io/developer-resources/api/rest-api/trade/updateSubAccountName) | `account` | Renames an existing subaccount | Change a subaccount's display name for better organization | #### Delegation Management | Method | Type | Description | Purpose | | ---------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------ | --------------------------------------------------------------------- | | [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/addDelegatedSigner) | `delegation` | Adds a delegated signer to a subaccount | Grant trading permissions to bots, team members, or automated systems | | [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegatedSigners) | `delegation` | Retrieves all delegated signers for a subaccount | View current access permissions and audit delegated authority | | [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/removeDelegatedSigner) | `delegation` | Removes a delegated signer from a subaccount | Revoke access permissions immediately for security or team changes | #### Account Queries | Method | Type | Description | Purpose | | ---------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) | `query` | Retrieves deposit and withdrawal history | View historical balance changes from deposits and withdrawals | | [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) | `query` | Retrieves funding payment history and statistics | Monitor funding payments received/paid and analyze funding rate impact | | [Get Open Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOpenOrders) | `query` | Retrieves currently open orders for subaccount | Monitor active orders in the orderbook | | [Get Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOrdersHistory) | `query` | Retrieves orders with flexible filtering (open, closed, cancelled) | Monitor all orders with comprehensive status filtering and pagination | | [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) | `query` | Retrieves current and historical trading positions | Monitor active positions, profit/loss, risk metrics, and position history | | [Get Position History](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositionHistory) | `query` | Retrieves closed position history with pagination | Review previously closed positions with time-based filtering | | [Get Performance History](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPerformanceHistory) | `query` | Retrieves historical account value, PnL, and volume for a period | Analyze performance trends for charting and reporting | | [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) | `query` | Retrieves information and balances for a specific subaccount | View account structure, margin status, and available balances for a single subaccount | | [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) | `query` | Retrieves trade execution history (fills) | View historical trade executions, analyze trading performance, and reconcile positions | import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Modify Order Modify existing orders by changing their price and/or quantity. This is essentially a cancel-and-replace operation that maintains the order's position in the queue while updating its parameters. ### Request #### Request Format ```json { "params": { "action": "modifyOrder", "subAccountId": "1867542890123456789", "orderId": "2026771048053084160", "price": "45000.50", "quantity": "2.50" }, "nonce": 1703123456789, "signature": { "v": 27, "r": "0x5a3c2b1d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789", "s": "0x1a2b3c4d5e6f7890abcdef0123456789abcdef0123456789abcdef0123456789" }, "expiresAfter": 1703123456 } ``` #### Request Parameters | Parameter | Type | Required | Description | | ----------------------------------------------------------------------------------------------------------- | ------ | -------- | -------------------------------------------------------------------- | | `params` | object | Yes | Action object containing modification details | | `params.action` | string | Yes | Must be `"modifyOrder"` | | `params.subAccountId` | string | Yes | SubAccount ID that owns the order | | `params.orderId` | string | Yes | Order ID to modify (string for JS BigInt compatibility) | | `params.price` | string | No\* | New price as decimal string | | `params.quantity` | string | No\* | New quantity as decimal string | | `params.triggerPrice` | string | No\* | New trigger price for conditional orders (triggerSl, triggerTp) | | `nonce` | number | Yes | Positive integer, incrementing nonce for signature replay protection | | `signature` | object | Yes | EIP-712 signature components | | `expiresAfter` | number | No | Optional expiration timestamp in seconds | | \*At least one of `price`, `quantity`, or `triggerPrice` must be provided for the modification to be valid. | | | | ### Response #### Success Response ```json { "status": "ok", "response": { "order": { "venueId": "2026771048053084160", "clientId": "cli-12345" }, "orderId": "2026771048053084160", "status": "modified", "price": "45000.50", "quantity": "2.50", "timestamp": 1703123456789 }, "request_id": "req-123456789" } ``` **With optional fields (for partially filled orders):** ```json { "status": "ok", "response": { "order": { "venueId": "2026771048053084160", "clientId": "cli-12345" }, "orderId": "2026771048053084160", "status": "modified", "price": "45000.50", "quantity": "2.50", "cumQty": "0.50", "avgPrice": "45100.25", "timestamp": 1703123456789 }, "request_id": "req-123456789" } ``` #### Rejection Response Trading rejections return HTTP 200 with `status: "rejected"` on the response object: ```json { "status": "ok", "response": { "order": { "venueId": "2026771048053084160", "clientId": "cli-12345" }, "orderId": "2026771048053084160", "status": "rejected", "error": "order 2026771048053084160 not found", "errorCode": "ORDER_NOT_FOUND", "timestamp": 1703123456789 }, "request_id": "req-123456789" } ``` #### Error Response Request-level errors return an error status: ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "Order not found or does not belong to this subaccount", "details": null }, "request_id": "req-123456789" } ``` #### Response Fields | Field | Type | Description | | ------------------------- | ------- | --------------------------------------------------------------- | | `status` | string | Always `"ok"` for successful requests or `"error"` for failures | | `response` | object | Contains operation results (omitted when `status` is `"error"`) | | `response.order` | object | Canonical modified order identifier object | | `response.order.venueId` | string | Canonical venue-generated order ID | | `response.order.clientId` | string | Optional client-provided order ID | | `response.orderId` | string | Deprecated legacy modified order ID | | `response.status` | string | Always `"modified"` for successful operations | | `response.price` | string | New price (only if modified) | | `response.quantity` | string | New quantity (only if modified) | | `response.triggerPrice` | string | New trigger price (only if modified) | | `response.cumQty` | string | Cumulative filled quantity (optional) | | `response.avgPrice` | string | Average fill price (optional) | | `response.timestamp` | integer | Unix milliseconds timestamp | | `error` | object | Error details (only present when `status` is `"error"`) | | `request_id` | string | Request tracking identifier | **Migration Note**: Use `response.order.venueId` as the canonical identifier. `response.orderId` remains as a deprecated compatibility field. ### Code Examples #### Modify Order Price Only ```json { "params": { "action": "modifyOrder", "subAccountId": "1867542890123456789", "orderId": "12345", "price": "49000.00" }, "nonce": 1703123456789, "signature": { "v": 27, "r": "0x5a3c2b1d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789", "s": "0x1a2b3c4d5e6f7890abcdef0123456789abcdef0123456789abcdef0123456789" } } ``` #### Modify Order Quantity Only ```json { "params": { "action": "modifyOrder", "subAccountId": "1867542890123456789", "orderId": "12346", "quantity": "0.50" }, "nonce": 1703123466789, "signature": { "v": 27, "r": "0x5a3c2b1d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789", "s": "0x1a2b3c4d5e6f7890abcdef0123456789abcdef0123456789abcdef0123456789" } } ``` #### Modify Both Price and Quantity ```json { "params": { "action": "modifyOrder", "subAccountId": "1867542890123456789", "orderId": "12347", "price": "95.50", "quantity": "5.00" }, "nonce": 1703123476789, "signature": { "v": 27, "r": "0x5a3c2b1d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789", "s": "0x1a2b3c4d5e6f7890abcdef0123456789abcdef0123456789abcdef0123456789" }, "expiresAfter": 1703123506 } ``` #### Modify Trigger Price for Conditional Order ```json { "params": { "action": "modifyOrder", "subAccountId": "1867542890123456789", "orderId": "12348", "triggerPrice": "48000.00" }, "nonce": 1703123486789, "signature": { "v": 27, "r": "0x5a3c2b1d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789", "s": "0x1a2b3c4d5e6f7890abcdef0123456789abcdef0123456789abcdef0123456789" } } ``` ### Important Behavior * **Queue Position**: The operation preserves the order's position in the queue when possible * **Atomic Operation**: The modify operation is atomic - either the entire modification succeeds or fails * **Partial Modification**: If only one field (price or quantity) is provided, the other field retains its current value * **Precision**: All monetary values use string representation to avoid floating-point precision issues ### Validation Rules #### Request Validation 1. **Action Type**: Must be exactly `"modifyOrder"` 2. **Order ID**: Must be a valid positive integer string 3. **Modification Fields**: At least one of `price`, `quantity`, or `triggerPrice` must be provided 4. **Price Format**: If provided, must be a valid decimal string 5. **Quantity Format**: If provided, must be a valid decimal string 6. **Trigger Price Format**: If provided, must be a valid decimal string 7. **Order Existence**: The order must exist and belong to the specified subaccount 8. **Signature Verification**: EIP-712 signature must be valid for the request parameters #### Business Logic Validation * Order must be in an open/active state * Cannot modify orders that are already filled or cancelled * Price/quantity changes must comply with market rules * Account must have sufficient margin for the modification #### Authentication & Authorization * Request must include valid EIP-712 signature using the ModifyOrder typed data structure * `nonce` must be a positive integer, incrementing and unique per request * Each nonce can only be used once (replay protection) * `expiresAfter` timestamp must be in the future (if provided) **EIP-712 Signature Structure:** ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { ModifyOrder: [ { name: "subAccountId", type: "uint256" }, { name: "orderId", type: "uint256" }, { name: "price", type: "string" }, // Use empty string "" if not modifying { name: "quantity", type: "string" }, // Use empty string "" if not modifying { name: "triggerPrice", type: "string" }, // Use empty string "" if not modifying { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", orderId: "12345", price: "45000.50", quantity: "2.50", triggerPrice: "", // Empty string when not modifying nonce: Date.now(), expiresAfter: Math.floor(Date.now() / 1000) + 3600 // 1 hour from now }; const signature = await signer._signTypedData(domain, types, message); ``` ### Error Handling Common error scenarios: | Error Code | Description | | -------------------------- | ---------------------------------------------------------------- | | `VALIDATION_ERROR` | Request validation failed (invalid format, missing fields, etc.) | | `INVALID_FORMAT` | Request body is not valid JSON | | `INTERNAL_ERROR` | Server-side processing error | | `UNAUTHORIZED` | Authentication failed | | `FORBIDDEN` | Wallet does not own the specified subaccount | | Invalid signature | EIP-712 signature verification failed | | Signature address mismatch | Signature address does not match wallet address | | Nonce already used | Nonce has been used in a previous request (replay protection) | | Request expired | `expiresAfter` timestamp has passed | | Order not found | Order ID does not exist or not owned by user | | Order not modifiable | Order is filled, cancelled, or not a limit order | | Invalid order parameters | New order parameters are invalid | import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Place Orders Place one or more orders on Synthetix's orderbook. This endpoint always accepts an array of orders for consistency, even when placing a single order. ### Request #### Request Format ```json { "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [ { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "closePosition": false, "clientOrderId": "0x1234567890abcdef1234567890abcdef" } ], "grouping": "na", "source": "direct" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` ### Request Parameters #### Action Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Parameters object containing method details | | `params.action` | string | Yes | Must be `"placeOrders"` | | `params.subAccountId` | string | Yes | Subaccount identifier | | `params.orders` | array | Yes | Array of order objects (minimum 1 order) | | `params.grouping` | string | No | Order grouping: `"na"`, `"normalTpsl"`, `"positionTpsl"`, `"twap"` | | `params.source` | string | No | Request source identifier for tracking and rebates (max 100 characters). Use `"direct"` for generic integrations | #### Common Request Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------- | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `signature` | object | Yes | EIP-712 signature object | | `expiresAfter` | integer | No | Expiration timestamp (5x rate limit on stale cancels) | #### Order Object | Parameter | Type | Required | Description | | ------------------ | ------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `symbol` | string | Yes | Market symbol (e.g., `"BTC-USDT"`, `"ETH-USDT"`) | | `side` | string | Yes | `"buy"` or `"sell"` | | `orderType` | string | Yes | One of: `limitGtc`, `limitIoc`, `limitAlo`, `limitGtd`, `market`, `triggerSl`, `triggerTp`, `twap` | | `price` | string | Yes | Price as string. Use empty string when not applicable (market orders and trigger market) | | `quantity` | string | Yes | Order size as string | | `reduceOnly` | boolean | Yes | Only reduce existing position | | `postOnly` | boolean | Conditional | Whether order must be maker (no immediate match). Only used for `limitGtc` and `limitGtd` orders. Must be `false` for all other order types | | `triggerPrice` | string | Yes | Trigger price as string. Required for `triggerSl` and `triggerTp`; empty string otherwise | | `isTriggerMarket` | boolean | Yes | Execution type for trigger orders. `true` for market-on-trigger, `false` for limit-on-trigger. Must be `false` for non-trigger types | | `closePosition` | boolean | Yes | Close entire position when triggered (TP/SL orders only). Must be `false` for non-trigger types | | `clientOrderId` | string | No | 128-bit hex string (0x + 32 hex chars) | | `triggerPriceType` | string | Conditional | Price type used for trigger evaluation: `"mark"` or `"last"`. Defaults to `"mark"` when omitted. Applies to `triggerSl` and `triggerTp` orders only | | `expiresAt` | integer | Conditional | Expiration timestamp in Unix seconds. Required for `limitGtd` orders; must be 10 seconds to 24 hours in the future. Not valid for other order types | | `durationSeconds` | integer | Conditional | Total TWAP execution window in seconds. Must be positive. Required for `twap` orders only | **Note on `postOnly`**: The `postOnly` field is a request parameter but is **not included in the EIP-712 signed `Order` type**. It is processed server-side after signature verification. The EIP-712 `Order` type contains: `symbol`, `side`, `quantity`, `orderType`, `price`, `triggerPrice`, `reduceOnly`, `isTriggerMarket`, `clientOrderId`, `closePosition`. ### Order Type Enum and Rules * `limitGtc`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitIoc`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitAlo`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitGtd`: price required; `isTriggerMarket=false`; `triggerPrice=""`; `expiresAt` required (10s–24h in the future); `postOnly` accepted * `market`: price=""; `isTriggerMarket=false`; `triggerPrice=""` * `triggerSl`: `triggerPrice` required; `isTriggerMarket` determines execution; if `true` then `price=""`, if `false` then `price` required; optional `triggerPriceType` (`"mark"` or `"last"`, defaults to `"mark"`) * `triggerTp`: same rules as `triggerSl` * `twap`: `triggerPrice` must be empty; `durationSeconds` required (positive integer); optional `price` for limit TWAP execution price; use `grouping: "twap"` #### Examples ##### Limit GTC ```json { "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "isTriggerMarket": false, "postOnly": false, "closePosition": false } ``` ##### Limit GTC (Post-Only) ```json { "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "isTriggerMarket": false, "postOnly": true, "closePosition": false } ``` ##### Limit GTD (Good Till Date) ```json { "orderType": "limitGtd", "price": "50000.00", "triggerPrice": "", "isTriggerMarket": false, "postOnly": false, "closePosition": false, "expiresAt": 1704070800 } ``` ##### Market ```json { "orderType": "market", "price": "", "triggerPrice": "", "isTriggerMarket": false, "closePosition": false } ``` ##### Trigger SL (Market) ```json { "orderType": "triggerSl", "price": "", "triggerPrice": "50000", "isTriggerMarket": true, "closePosition": true } ``` ##### Trigger TP (Limit) ```json { "orderType": "triggerTp", "price": "49950.00", "triggerPrice": "50000.00", "isTriggerMarket": false, "closePosition": true } ``` ### Time in Force (TIF) Options | Value | Name | Description | | ---------- | ------------------- | ------------------------------------------------------------------------ | | `gtc` | Good Till Canceled | Order remains active until filled or canceled | | `gtd` | Good Till Date | Order active until filled, canceled, or `expiresAt` timestamp reached | | `ioc` | Immediate or Cancel | Fill immediately available quantity, cancel remainder | | `postOnly` | Post-Only | Set `postOnly: true` on `limitGtc` orders to ensure maker-only execution | ### Response #### Response Structure | Field | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------------------- | | `status` | string | `"ok"` for processed requests or `"error"` for request-level failures | | `response` | object | Contains operation results (omitted when `status` is `"error"`) | | `response.statuses` | array | Array of status objects, one per order submitted (check each for success/error) | | `error` | object | Error details (only present when `status` is `"error"`) | | `requestId` | string | Request tracking identifier | | `traceId` | string | Trace ID for debugging | | `timestamp` | integer | Unix milliseconds timestamp | #### Order Status Types Each order in the request receives one status response: | Status Type | Description | Fields | | ----------- | ------------------------- | ------------------------------------------------------------------------------------------------------ | | `resting` | Order placed in orderbook | `order` (canonical), `id` (deprecated), `expiresAt` (optional, Unix ms, present for `limitGtd` orders) | | `filled` | Order completely filled | `order` (canonical), `id` (deprecated), `avgPrice`, `totalSize`, `expiresAt` (optional, Unix ms) | | `canceled` | Order was canceled | `order` (canonical), `id` (deprecated) | | `error` | Order rejected | `error` (message string), `errorCode` (machine-readable code) | #### Success Response Examples ##### Single Order - Resting ```json { "status": "ok", "response": { "statuses": [ { "resting": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } } ] }, "requestId": "5ccf215d37e3ae6d" } ``` ##### Single Order - Filled ```json { "status": "ok", "response": { "statuses": [ { "filled": { "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "id": "1948058938469519361", "avgPrice": "50001.25", "totalSize": "0.1" } } ] }, "requestId": "5ccf215d37e3ae6d" } ``` ##### Single Order - Canceled ```json { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519362", "clientId": "cli-1948058938469519362" }, "id": "1948058938469519362" } } ] }, "requestId": "5ccf215d37e3ae6d" } ``` ##### Multiple Orders - Mixed Results (Partial Success) Batch requests can have mixed results where some orders succeed and others fail. The response still has `status: "ok"` because the request was processed successfully—check each item in the `statuses` array: ```json { "status": "ok", "response": { "statuses": [ { "resting": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } }, { "filled": { "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "id": "1948058938469519361", "avgPrice": "2999.50", "totalSize": "1.0" } }, { "error": "insufficient margin: additional needed 500.00, available 100.12", "errorCode": "INSUFFICIENT_MARGIN", "order": { "venueId": null, "clientId": "cli-1948058938469519362" } } ] }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` See [Batch Request Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling#batch-request-error-handling) for details on handling partial success scenarios. **Migration Note**: Use `*.order.venueId` as canonical in each status payload. Legacy `id` remains for compatibility. #### Error Response Request-level errors return an error status. For per-item errors in batch requests, see [Batch Request Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling#batch-request-error-handling). ```json { "status": "error", "error": { "code": "UNAUTHORIZED", "message": "Invalid signature", "category": "AUTH", "retryable": false }, "requestId": "5ccf215d37e3ae6d", "traceId": "abc123def456", "timestamp": 1704067200000 } ``` ### Examples #### Place Single Limit Order (GTC) ```json { "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [ { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "closePosition": false } ] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Place Single Limit GTD Order ```json { "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [ { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtd", "price": "50000.00", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "closePosition": false, "expiresAt": 1704070800 } ] }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Place Multiple Orders (Batch) — Mixed types ```json { "params": { "action": "placeOrders", "subAccountId": "1867542890123456789", "orders": [ { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "49000.00", "triggerPrice": "", "quantity": "0.05", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "closePosition": false }, { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "48000.00", "triggerPrice": "", "quantity": "0.1", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "closePosition": false }, { "symbol": "BTC-USDT", "side": "sell", "quantity": "0.15", "reduceOnly": true, "orderType": "triggerSl", "price": "", "triggerPrice": "45000", "isTriggerMarket": true, "closePosition": false } ], "grouping": "na" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` \:::info Rate Limiting This batch contains 3 orders, so it consumes 12 tokens (3 × 4) from your per-subaccount bucket. See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for details. \::: ### Implementation Notes * Order nonces must be positive integers, incrementing and unique per request * Client order IDs enable request tracking with 128-bit hex format (0x prefix + 32 hex characters) * Limit orders require price specification, trigger orders require trigger price * Batch operations process orders independently with individual status responses * Order grouping types: `"na"` (none), `"normalTpsl"` (stop-loss/take-profit), `"positionTpsl"` (position-based), `"twap"` (TWAP execution) * All price and quantity values must be provided as strings for precision preservation #### Linked TP/SL sibling cancellation When a take-profit or stop-loss order in a linked `normalTpsl` pair fills, the platform automatically cancels the sibling order **without creating a history entry**: * The filled leg (TP or SL) appears in `getOrdersHistory` with `status: "Filled"` as expected. * The auto-cancelled sibling does **not** appear in `getOrdersHistory`. Do not rely on an auto-cancelled sibling row appearing in order history when reconciling linked TP/SL pairs. ### Error Handling #### Common Errors | Error Code | Description | Solution | | ------------------------- | ------------------------------------ | ----------------------------------- | | `INSUFFICIENT_MARGIN` | Account lacks required margin | Add collateral or reduce order size | | `REQUEST_EXPIRED` | `expiresAfter` timestamp has passed | Use longer expiration or remove | | `INVALID_VALUE` | Market symbol not recognized | Check supported markets | | `QUANTITY_TOO_SMALL` | Below minimum order size | Check market minimums | | `POST_ONLY_WOULD_TRADE` | Post-only order would take liquidity | Adjust price or use different TIF | | `MAX_ORDERS_EXCEEDED` | Maximum order limit reached | Cancel existing orders first | | `REDUCE_ONLY_NO_POSITION` | No position for reduce-only order | Remove reduce-only flag | | `MARKET_CLOSED` | Market is not open for trading | Wait for market to open | #### Batch Order Errors For batch orders, each order is validated and processed independently: * **Request-level errors** (invalid signature, rate limit) fail the entire batch with an error response * **Per-item errors** (insufficient margin, invalid price) return HTTP 200 with individual error statuses * The `statuses` array preserves the order of your request—index 0 corresponds to your first order * Successfully processed orders are **not rolled back** if other orders fail See [Batch Request Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling#batch-request-error-handling) for complete details and code examples. ### Rate Limiting * **Each order in the batch consumes base cost** - A request with 5 orders consumes 5 × 4 = 20 tokens from your per-subaccount bucket * Bucket size: 1,000 tokens per 10 seconds per subaccount (see [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits)) * Failed orders due to validation errors still count against rate limits * See [Rate Limits](https://developers.synthetix.io/developer-resources/api/rate-limits) for complete details ### Next Steps * [Authentication](https://developers.synthetix.io/developer-resources/api/rest-api/trade/authentication) - Set up request signing * [WebSocket Trading](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Real-time alternative * [Error Handling](https://developers.synthetix.io/developer-resources/api/error-handling) - Handle errors properly import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Remove All Delegated Signers Remove all delegated signers from a subaccount in a single request, revoking their ability to perform trading actions on behalf of the subaccount. This immediately terminates all trading permissions previously granted to any wallet addresses, providing a fast way to revoke all delegations at once. ### Request #### Request Format ```json { "params": { "action": "removeAllDelegatedSigners", "subAccountId": "1867542890123456789" }, "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------ | | `params` | object | Yes | Action object containing removal details | | `params.action` | string | Yes | Must be `"removeAllDelegatedSigners"` | | `params.subAccountId` | string | Yes | The subaccount ID to remove all delegated signers from | | `expiresAfter` | integer | No | Optional request expiration timestamp (Unix seconds) | #### EIP-712 Signature Structure The request is signed using EIP-712 with the following type definition: ```typescript const RemoveAllDelegatedSignersTypes = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "removedSigners": [ "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E" ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ---------------- | --------- | ---------------------------------------------------------- | | `subAccountId` | string | The subaccount ID that delegated signers were removed from | | `removedSigners` | string\[] | Array of wallet addresses that were removed | #### No Delegations Response When there are no delegated signers to remove: ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "removedSigners": [] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Only master account can remove delegated signers", "code": "UNAUTHORIZED" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Common Error Cases ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Emergency Revocation of All Signers ```json { "params": { "action": "removeAllDelegatedSigners", "subAccountId": "1867542890123456789" }, "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes from now (Unix seconds) const message = { subAccountId: BigInt("1867542890123456789"), nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const signature = await signer._signTypedData(domain, types, message); ``` ### Implementation Notes * **Immediate Effect**: All delegations are revoked immediately upon successful execution * **Access Requirements**: Only master account owners can remove delegated signers * **Atomic Operation**: Either all delegations are removed or none are (transaction is atomic) * **Idempotent**: Calling when no delegations exist returns a success response with an empty `removedSigners` array * **Signature Required**: All removal operations require valid EIP-712 signatures * **Recovery**: Removed signers can be re-added individually through the `addDelegatedSigner` endpoint ### Use Cases * **Emergency Lockdown**: Quickly revoke all delegated access in a security event * **Account Reset**: Clear all delegations before setting up new access patterns * **Team Offboarding**: Remove all team member access when restructuring * **Security Rotation**: Revoke all existing delegations before re-adding with new permissions ### Comparison with removeDelegatedSigner | Feature | `removeDelegatedSigner` | `removeAllDelegatedSigners` | | ---------- | -------------------------- | ----------------------------- | | Scope | Single delegate address | All delegated signers | | Parameters | Requires `delegateAddress` | No address needed | | Use Case | Targeted revocation | Bulk emergency revocation | | Response | Single address confirmed | List of all removed addresses | ### Related Endpoints * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/removeDelegatedSigner) - Remove a single delegated signer * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegatedSigners) - List all delegated signers * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/rest-api/trade/addDelegatedSigner) - Add a new delegation * [Get Subaccounts](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccounts) - View all subaccounts with delegation info ### Error Handling import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Remove Delegated Signer Remove a delegated signer from a subaccount, revoking their ability to perform trading actions on behalf of the subaccount. This immediately terminates the trading permission previously granted to the specified wallet address. ### Request #### Request Format ```json { "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590" }, "nonce": 1735689600000, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `params` | object | Yes | Action object containing removal details | | `params.action` | string | Yes | Must be `"removeDelegatedSigner"` | | `params.subAccountId` | string | Yes | The subaccount ID to remove the delegated signer from | | `params.walletAddress` | string | Yes | Ethereum wallet address of the delegated signer to remove (42-character hex format). Note: this request field is `walletAddress`; the EIP-712 signed message uses `delegateAddress` for the same value | | `expiresAfter` | integer | No | Optional request expiration timestamp (milliseconds) | #### EIP-712 Signature Structure The request is signed using EIP-712 with the following type definition: ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Cascade Removal Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "cascadeRemovedSigners": [ "0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222" ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ----------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `subAccountId` | string | The subaccount ID the signer was removed from | | `walletAddress` | string | The removed delegated signer's wallet address | | `cascadeRemovedSigners` | string\[] \| omitted | Wallet addresses of sub-delegates automatically removed because they were created by the removed signer. Omitted when empty | #### Error Response ```json { "status": "error", "error": { "message": "Delegated signer not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Common Error Cases ```json { "status": "error", "error": { "message": "Only master account can remove delegated signers", "code": "UNAUTHORIZED" }, "request_id": "5ccf215d37e3ae6d" } ``` ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Remove Trading Bot Access ```json { "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590" }, "nonce": 1735689600000, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Revoke Team Member Access ```json { "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E" }, "nonce": 1735689600001, "expiresAfter": 1735689900000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Implementation Notes * **Immediate Effect**: Removal takes effect immediately upon successful execution * **Access Requirements**: Only master account owners can remove delegated signers * **No Self-Removal**: Delegated signers cannot remove themselves (must be done by master account) * **Idempotent**: Attempting to remove a non-existent delegation returns an error * **Active Sessions**: Any active sessions or connections for the removed signer should be terminated * **Pending Operations**: Any pending operations initiated by the removed signer remain valid * **Audit Trail**: All removal actions are logged for security and compliance ### Security Considerations * **Master Account Only**: System validates that only the master account can remove delegated signers * **Address Validation**: Wallet addresses must be valid Ethereum addresses * **Signature Required**: All removal operations require valid EIP-712 signatures * **Cascade Removal**: If the removed signer created sub-delegates, the backend returns those automatically revoked addresses in `cascadeRemovedSigners`. The field is omitted when no cascade occurs. * **Recovery**: Removed signers can be re-added if needed through the `addDelegatedSigner` endpoint ### Effect on Active Operations | Operation Type | Effect of Removal | | ------------------- | -------------------------------------------------- | | Open Orders | Remain active (can be cancelled by master account) | | Pending Withdrawals | Continue processing | | Active Sessions | Should be terminated | | API Keys | Should be invalidated | | Subscriptions | Should be cancelled | ### Use Cases * **Security Response**: Immediately revoke access when a delegated signer is compromised * **Team Changes**: Remove access when team members leave or change roles * **Bot Decommission**: Remove trading bot access when no longer needed * **Access Rotation**: Regular removal and re-addition of signers for security * **Emergency Lockdown**: Quick removal of all delegated signers in security events ### Error Handling import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Schedule Cancel (Dead Man's Switch) Schedule an automatic cancel-all operation at a future time as a safety feature if you lose connectivity or system access. ### Requests #### Request Format ##### Schedule Cancel ```json { "params": { "action": "scheduleCancel", "subAccountId": "1867542890123456789", "timeoutSeconds": 300 }, "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ##### Cancel Dead Man's Switch ```json { "params": { "action": "scheduleCancel", "subAccountId": "1867542890123456789", "timeoutSeconds": 0 }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ----------------------- | ---------------- | -------- | ---------------------------------------------------------- | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"scheduleCancel"` | | `params.subAccountId` | string | Yes | SubAccount ID | | `params.timeoutSeconds` | integer | Yes | Timeout in seconds; use `0` to cancel an existing schedule | | `nonce` | integer (uint64) | Yes | Unix milliseconds timestamp, incrementing | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature object | #### Time Requirements * **Minimum timeout**: 5 seconds (`timeoutSeconds >= 5`) * **Maximum timeout**: 86400 seconds (24 hours) * **Cancel schedule**: set `timeoutSeconds: 0` ### Response #### Success Response - Scheduled ```json { "status": "ok", "response": { "isActive": true, "message": "dead-man-switch armed", "timeoutSeconds": 300, "triggerTime": 1704067500000 }, "request_id": "5ccf215d37e3ae6d", "requestId": "5ccf215d37e3ae6d", "timestamp": 1704067200123 } ``` #### Success Response - Cancelled ```json { "status": "ok", "response": { "isActive": false, "message": "dead-man-switch disabled", "timeoutSeconds": 0 }, "request_id": "5ccf215d37e3ae6d", "requestId": "5ccf215d37e3ae6d", "timestamp": 1704067200456 } ``` #### Error Response ```json { "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "timeoutSeconds must be less than or equal to 86400" }, "request_id": "5ccf215d37e3ae6d", "requestId": "5ccf215d37e3ae6d", "timestamp": 1704067200000 } ``` ### Code Examples #### Schedule Cancel in 5 Minutes ```json { "params": { "action": "scheduleCancel", "subAccountId": "1867542890123456789", "timeoutSeconds": 300 }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Schedule Cancel in 1 Hour ```json { "params": { "action": "scheduleCancel", "subAccountId": "1867542890123456789", "timeoutSeconds": 3600 }, "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Dead Man's Switch Behavior * **Automatic Trigger**: All open orders are cancelled at the scheduled time * **Single Use**: Each schedule is used once and then removed * **Overwrite**: New schedule replaces existing one * **Cancellation**: Send `timeoutSeconds: 0` to cancel an existing schedule ### Use Cases * **Trading Bot Safety**: Cancel orders if bot loses connection * **Manual Trading**: Safety net for manual traders * **Risk Management**: Automatic order cancellation during volatile periods * **Maintenance Windows**: Clear orders before system maintenance ### Validation Rules * `nonce` must be a timestamp in milliseconds and must be increasing * `timeoutSeconds` must be `0` (cancel) or between 5 and 86400 (inclusive) * Only one schedule can be active at a time; a new schedule overwrites the existing one * Must be signed by account owner or authorized delegate ### Error Handling Common error scenarios: | Error | Description | | -------------------------- | ----------------------------------- | | `timeoutSeconds` too small | Must be 0 or at least 5 | | `timeoutSeconds` too large | Exceeds maximum of 86400 seconds | | Request expired | `expiresAfter` timestamp has passed | import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import CommonRequestParams from "../../../../../snippets/common-request-params.mdx"; import EIP712Signing from "../../../../../snippets/eip712-signing.mdx"; import ExampleSignature from "../../../../../snippets/example-signature.mdx"; import InfoEndpoints from "../../../../../snippets/info-endpoints.mdx"; import NonceManagement from "../../../../../snippets/nonce-management.mdx"; import TradeEndpoints from "../../../../../snippets/trade-endpoints.mdx"; ## Update Leverage Change your leverage setting for a specific symbol/market ### Request #### Request Format ```json { "params": { "action": "updateLeverage", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "leverage": "20" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------ | | `params` | object | Yes | Parameters object containing action details | | `params.action` | string | Yes | Must be `"updateLeverage"` | | `params.subAccountId` | string | Yes | Subaccount identifier | | `params.symbol` | string | Yes | Market symbol (e.g., `"BTC-USDT"`) | | `params.leverage` | string | Yes | Desired leverage multiplier (e.g., "10" for 10x) | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `signature` | object | Yes | EIP-712 signature object | | `expiresAfter` | integer | No | Expiration timestamp in seconds | #### EIP-712 Type Definition ```typescript const UpdateLeverageTypes = { UpdateLeverage: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "leverage", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response #### Success Response ```json { "status": "ok", "response": { "symbol": "BTC-USDT", "previousLeverage": "10", "newLeverage": "20" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Error Response ```json { "status": "error", "error": { "message": "Leverage exceeds maximum allowed", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Degen Leverage Setting ```json { "params": { "action": "updateLeverage", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "leverage": "100" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` #### Conservative Leverage Setting ```json { "params": { "action": "updateLeverage", "subAccountId": "1867542890123456789", "symbol": "SOL-USDT", "leverage": "5" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } ``` ### Important Behavior #### How Leverage Settings Work When you update leverage for a market: * The new leverage setting applies to **all current and future positions** in that market #### Key Points * Each symbol can have different leverage settings * Leverage persists across sessions (survives reconnections) * Higher leverage = higher liquidation risk * Lower leverage = more margin required ### Market-Specific Limits Different markets may have different maximum leverage limits: * Major pairs (BTC-USDT, ETH-USDT): Up to 50x * Other pairs: May have lower limits ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * `leverage` must be a positive integer * `leverage` must not exceed market maximum * `symbol` must be a valid market symbol in canonical uppercase format (e.g., `"BTC-USDT"`, not `"btc-usdt"`) * Must be signed by account owner or authorized delegate ### Error Handling Common error scenarios: | Error | Description | | -------------------------------- | ------------------------------------ | | Leverage exceeds maximum allowed | Leverage setting too high for market | | Invalid market symbol | Market symbol not recognized | | Invalid leverage value | Leverage must be positive integer | | Request expired | `expiresAfter` timestamp has passed | | Not enough margin | Going from 100x to 1x | import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Update Subaccount Name Rename an existing trading subaccount. The authenticated subaccount's display name is updated to the new value provided in the request. ### Request #### Request Format ```json { "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Scalping Strategy" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ---------------- | -------- | ----------------------------------------------------- | | `params` | object | Yes | Parameters object containing action details | | `params.action` | string | Yes | Must be `"updateSubAccountName"` | | `params.subAccountId` | string | Yes | Subaccount identifier to rename | | `params.name` | string | Yes | New display name for the subaccount | | `nonce` | integer (uint64) | Yes | Unix milliseconds timestamp, monotonically increasing | | `signature` | object | Yes | EIP-712 signature object | | `expiresAfter` | integer | No | Expiration timestamp in seconds | #### EIP-712 Type Definition ```typescript const UpdateSubAccountNameTypes = { UpdateSubAccountName: [ { name: "subAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response #### Success Response ```json { "status": "ok", "response": { "subAccountId": "1867542890123456789", "name": "Scalping Strategy" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | -------------- | ------ | -------------------------------------- | | `subAccountId` | string | Subaccount identifier that was renamed | | `name` | string | The new display name that was applied | #### Error Response ```json { "status": "error", "error": { "message": "subAccountId is required", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` #### Common Error Cases ```json { "status": "error", "error": { "message": "Invalid request body", "code": "INVALID_FORMAT" }, "request_id": "5ccf215d37e3ae6d" } ``` ```json { "status": "error", "error": { "message": "Subaccount not found", "code": "NOT_FOUND" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Code Examples #### Rename Subaccount ```json { "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Main Trading" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Rename to Strategy-Based Name ```json { "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Grid Trading Bot" }, "nonce": 1704067200001, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067301 } ``` ### Validation Rules * `nonce` must be a timestamp in milliseconds and must be increasing * `name` must be a non-empty string, maximum 50 characters, containing only alphanumeric characters and `-`, `.`, `=`, `/`, `+`, `_`; leading and trailing whitespace is trimmed * `subAccountId` must reference an existing subaccount owned by the signer * Must be signed by account owner or authorized delegate * Name must not already be in use by another subaccount under the same wallet ### Error Handling | Error | Description | | --------------------------- | ------------------------------------------ | | subAccountId is required | No subaccount was specified in the request | | Invalid request body | Request payload could not be parsed | | Subaccount not found | The specified subaccount does not exist | | Name already in use | Another subaccount already uses this name | | Failed to update subaccount | Internal error during the update operation | import CollateralValueHaircut from '../../../../../snippets/collateral-value-haircut.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Voluntary Auto Exchange Exchange non-USDT collateral for USDT at the best available rate, with automatic haircut calculation applied to the source asset. ### Request #### Request Format ```json { "params": { "action": "voluntaryAutoExchange", "sourceAsset": "WETH", "targetUSDTAmount": "5000.0" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `params` | object | Yes | Parameters object containing action details | | `params.action` | string | Yes | Must be `"voluntaryAutoExchange"` | | `params.sourceAsset` | string | Yes | Collateral asset to exchange (e.g., `"WETH"`, `"cbBTC"`, `"sUSDe"`). Cannot be `"USDT"` | | `params.targetUSDTAmount` | string | Yes | Amount of USDT to receive as a positive decimal string (e.g., `"5000.0"`), or `"all"` to exchange the entire source asset balance | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `signature` | object | Yes | EIP-712 signature object | | `expiresAfter` | integer | No | Expiration timestamp in seconds | :::info `subAccountId` is **not** included in the `params` object for this action. It is derived from your authenticated session and must be included in the EIP-712 signed message. ::: #### EIP-712 Type Definition ```typescript const VoluntaryAutoExchangeTypes = { VoluntaryAutoExchange: [ { name: "subAccountId", type: "uint256" }, { name: "sourceAsset", type: "string" }, { name: "targetUSDTAmount", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response #### Success Response ```json { "status": "ok", "response": { "sourceAsset": "WETH", "sourceAmountTaken": "2.0202", "targetAsset": "USDT", "targetAmount": "5000.0", "indexPrice": "2475.0", "effectiveHaircut": "0.01", "collateral": [ { "symbol": "WETH", "quantity": "7.9798" }, { "symbol": "USDT", "quantity": "15000.00" } ] }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | ----------------------- | ------ | ------------------------------------------------------ | | `sourceAsset` | string | Collateral asset that was exchanged | | `sourceAmountTaken` | string | Amount of source asset deducted, including the haircut | | `targetAsset` | string | Asset received (always `"USDT"`) | | `targetAmount` | string | Amount of USDT received | | `indexPrice` | string | Index price used for the exchange | | `effectiveHaircut` | string | Haircut fraction applied (e.g., `"0.01"` = 1%) | | `collateral` | array | Updated collateral balances after the exchange | | `collateral[].symbol` | string | Asset symbol | | `collateral[].quantity` | string | Remaining balance of the asset | #### Error Response ```json { "status": "error", "error": { "message": "sourceAsset cannot be USDT", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d" } ``` ### Exchange Rules * Voluntary trader-initiated exchange with immediate execution * Uses the best available index price at execution time * Automatic haircut applied based on collateral tier (see table below) * No partial fills — full target amount received or request rejected ### Validation Rules * `sourceAsset` must be a supported collateral type and cannot be `"USDT"` * `targetUSDTAmount` must be a positive decimal string or `"all"` * Account must have sufficient source asset balance including the haircut component (amount taken = targetUSDT / indexPrice / (1 - haircut)) * Exchange must maintain minimum margin requirements post-execution ### Error Handling | Error | Description | | --------------------------- | ------------------------------------------------------------- | | Source asset cannot be USDT | `sourceAsset` must be a non-USDT collateral type | | Source asset is required | `sourceAsset` field is missing or empty | | Invalid target amount | `targetUSDTAmount` must be a positive decimal or `"all"` | | Insufficient balance | Account lacks required source asset balance including haircut | | Margin violation | Exchange would violate minimum margin requirements | import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; ## Withdraw Collateral Withdraw collateral from your Synthetix trading account back to your wallet, subject to approval, margin requirements and risk checks. ### Request #### Request Format ```json { "params": { "action": "withdrawCollateral", "subAccountId": "1867542890123456789", "symbol": "USDT", "amount": "1000.0", "destination": "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2" }, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" }, "expiresAfter": 1704067300 } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | --------------------------------------------------------- | | `params` | object | Yes | Action object containing withdrawal details | | `params.action` | string | Yes | Must be `"withdrawCollateral"` | | `params.subAccountId` | string | Yes | Subaccount identifier | | `params.symbol` | string | Yes | Collateral asset symbol to withdraw (e.g., "USDT", "ETH") | | `params.amount` | string | Yes | Amount to withdraw as decimal string (e.g., "1000.0") | | `params.destination` | string | Yes | Destination wallet address (0x-prefixed Ethereum address) | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `signature` | object | Yes | EIP-712 signature object | | `expiresAfter` | integer | No | Expiration timestamp in seconds (optional) | :::info Withdrawals are sent to the specified destination address. ::: ### Response #### Success Response ```json { "status": "ok", "response": { "requestId": "5ccf215d37e3ae6d", "symbol": "USDT", "amount": "1000.0", "destination": "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2", "timestamp": 0, "withdrawTime": 0 }, "request_id": "5ccf215d37e3ae6d" } ``` #### Response Fields | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------------------------------ | | `requestId` | string | Echoes the client `request_id` from the response envelope for end-to-end correlation | | `symbol` | string | Asset symbol being withdrawn | | `amount` | string | Amount being withdrawn | | `destination` | string | Destination wallet address | | `timestamp` | integer | Deprecated — always returns `0` (pending removal) | | `withdrawTime` | integer | Withdrawal processing timestamp in milliseconds — currently returns `0` | #### Error Response ```json { "status": "error", "error": { "message": "Insufficient withdrawable balance", "code": "VALIDATION_ERROR" }, "request_id": "5ccf215d37e3ae6d", "timestamp": "2025-01-01T00:00:00Z" } ``` ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * `symbol` must be a valid collateral symbol (e.g., "USDT", "ETH") * `amount` must be a positive number as string * `destination` must be a valid Ethereum address (0x-prefixed) * `subAccountId` must be a valid subaccount owned by or delegated to the signing account * Account must have sufficient withdrawable balance of the specified asset * Withdrawal must not violate margin requirements * Must be signed by account owner (no delegate support) :::warning Withdrawals must be signed by the account owner. Delegate addresses cannot initiate withdrawals. ::: #### EIP-712 Signature Structure The withdrawal request fields are signed using EIP-712: ##### WithdrawCollateral Type Definition ```typescript const WithdrawCollateralTypes = { WithdrawCollateral: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "amount", type: "string" }, { name: "destination", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ##### Example Typed Data for WithdrawCollateral ```json { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "WithdrawCollateral": [ { "name": "subAccountId", "type": "uint256" }, { "name": "symbol", "type": "string" }, { "name": "amount", "type": "string" }, { "name": "destination", "type": "address" }, { "name": "nonce", "type": "uint256" }, { "name": "expiresAfter", "type": "uint256" } ] }, "primaryType": "WithdrawCollateral", "domain": { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" }, "message": { "subAccountId": "1867542890123456789", "symbol": "USDT", "amount": "1000.0", "destination": "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2", "nonce": 1704067200000, "expiresAfter": 1704067300 } } ``` **Important Notes**: * The `amount` field is a string type and should match the decimal representation from the request (e.g., "1000.0") * The `expiresAfter` field should match the request value, or be set to `0` when not provided (representing no expiration) * The `nonce` field is included in the EIP-712 message and matches the top-level request field * All withdrawal operations must be signed by the master account owner * For testnet, use `chainId: 11155111` (Ethereum Sepolia). See [Environments](https://developers.synthetix.io/environments) for complete configuration details ### Error Handling Common error scenarios: | Error | Description | | --------------------------------- | ------------------------------------------------ | | Insufficient withdrawable balance | Account lacks required funds for specified asset | | Margin requirement violation | Withdrawal would violate margin rules | | Invalid asset symbol | Asset type not supported as collateral | | Account health check failed | Withdrawal would make account unhealthy | | Delegate signature not allowed | Must be signed by account owner | | Minimum withdrawal amount | Amount below minimum threshold | ### Related * [Deposits](https://developers.synthetix.io/deposits) - How to deposit collateral into your account * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - View deposit and withdrawal history * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Check current balances and margin status import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Candles (WebSocket) Retrieve OHLCV (Open, High, Low, Close, Volume) price data for a specific trading pair through the WebSocket connection. This endpoint provides historical candlestick data for technical analysis and charting without requiring authentication. ### Request #### Request Format ```json { "id": "candles-1", "method": "post", "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "1h", "limit": 500, "startTime": 1704067200000, "endTime": 1704153600000 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getCandles"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`) | | `params.interval` | string | Yes | Time interval: `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `8h`, `12h`, `1d`, `3d`, `1w`, `1M`, `3M` | | `params.limit` | integer | No | Number of candles to return (0 = no limit) | | `params.startTime` | integer | No | Start time in milliseconds (default: 24 hours ago) | | `params.endTime` | integer | No | End time in milliseconds (default: current time) | #### Valid Intervals | Interval | Description | Milliseconds | | -------- | ----------- | ------------ | | `1m` | 1 minute | 60,000 | | `5m` | 5 minutes | 300,000 | | `15m` | 15 minutes | 900,000 | | `30m` | 30 minutes | 1,800,000 | | `1h` | 1 hour | 3,600,000 | | `4h` | 4 hours | 14,400,000 | | `8h` | 8 hours | 28,800,000 | | `12h` | 12 hours | 43,200,000 | | `1d` | 1 day | 86,400,000 | | `3d` | 3 days | 259,200,000 | | `1w` | 1 week | 604,800,000 | | `1M` | 1 month | variable | | `3M` | 3 months | variable | ### Response #### Success Response ```json { "id": "candles-1", "status": 200, "result": { "response": { "symbol": "BTC-USDT", "interval": "1h", "candles": [ { "openTime": 1704067200000, "closeTime": 1704070800000, "openPrice": "45000.50", "highPrice": "45100.00", "lowPrice": "44950.00", "closePrice": "45050.00", "volume": "125.5", "quoteVolume": "125500.00", "tradeCount": 1523 }, { "openTime": 1704070800000, "closeTime": 1704074400000, "openPrice": "45050.00", "highPrice": "45200.00", "lowPrice": "45000.00", "closePrice": "45150.00", "volume": "98.2", "quoteVolume": "98450.00", "tradeCount": 1102 } ] }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | -------------------------- | ------ | ------------------------------------------- | | `result.response.symbol` | string | Trading pair symbol | | `result.response.interval` | string | Time interval used | | `result.response.candles` | array | Array of candle objects | | `result.status` | string | Always `"success"` for successful responses | #### Candle Object | Field | Type | Description | | ------------- | ------- | --------------------------------- | | `openTime` | integer | Candle open time in milliseconds | | `closeTime` | integer | Candle close time in milliseconds | | `openPrice` | string | Opening price | | `highPrice` | string | Highest price during the period | | `lowPrice` | string | Lowest price during the period | | `closePrice` | string | Closing price | | `volume` | string | Base asset trading volume | | `quoteVolume` | string | Quote asset trading volume | | `tradeCount` | integer | Number of trades in the period | #### Error Response ```json { "id": "candles-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid interval" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request hourly candles for BTC-USDT ws.send(JSON.stringify({ id: 'candles-1', method: 'post', params: { action: 'getCandles', symbol: 'BTC-USDT', interval: '1h', limit: 100 } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'candles-1' && response.status === 200) { const data = response.result.response; console.log(`${data.symbol} ${data.interval} candles:`, data.candles); // Process candle data data.candles.forEach(candle => { console.log(`Open: ${candle.openPrice}, High: ${candle.highPrice}, Low: ${candle.lowPrice}, Close: ${candle.closePrice}`); }); } }; ``` ### Examples #### Get Recent 1-Hour Candles ```json { "id": "candles-hourly", "method": "post", "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "1h", "limit": 100 } } ``` #### Get Daily Candles for Date Range ```json { "id": "candles-daily", "method": "post", "params": { "action": "getCandles", "symbol": "ETH-USDT", "interval": "1d", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 30 } } ``` #### Get 5-Minute Candles ```json { "id": "candles-5m", "method": "post", "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "5m", "limit": 200 } } ``` #### Get All Candles (No Limit) ```json { "id": "candles-all", "method": "post", "params": { "action": "getCandles", "symbol": "BTC-USDT", "interval": "1h", "limit": 0, "startTime": 1704067200000, "endTime": 1704153600000 } } ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Symbol Required**: A valid market symbol must be provided * **Interval Required**: A valid time interval must be specified * **Limit Usage**: Set limit to 0 for no limit, or specify desired number of candles * **Time Range**: Optional start and end time filtering with millisecond precision * **Sorting**: Candles are returned in ascending order by time (oldest first) ### Use Cases #### Technical Analysis * Chart patterns and trend identification * Technical indicator calculations (RSI, MACD, moving averages) * Support and resistance level identification #### Trading Strategies * Backtesting algorithmic strategies * Entry and exit point determination * Volatility analysis #### Market Research * Historical price movement analysis * Volume trend analysis * Market behavior patterns #### Risk Management * Volatility assessment * Drawdown calculations * Portfolio performance tracking ### Validation Rules * No authentication required (public Info WebSocket) * Symbol parameter is required and must be a valid market symbol * Interval parameter is required and must be one of the valid intervals * Limit must be non-negative (≥ 0), where 0 means no limit * If both startTime and endTime are provided, startTime must be less than endTime ### Common Errors | Error | Description | | ----------------------- | ---------------------------- | | Symbol required | Missing trading pair symbol | | Invalid interval | Unsupported time interval | | Invalid limit | Limit must be non-negative | | Invalid time range | Start time is after end time | | Failed to retrieve data | Internal service error | ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **Efficient Queries**: Use specific time ranges to reduce data transfer * **Caching**: Historical candle data is immutable and suitable for caching ### Related Endpoints * [Candle Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/candleUpdates) - Real-time candle updates via subscription * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMids) - Current mid prices * [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) - Available trading pairs * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) - HTTP request for candles import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; import FundingRateObject from '../../../../../snippets/funding-rate-object.mdx'; ## Get Funding Rate (WebSocket) Retrieve current funding rates for perpetual markets through the WebSocket connection. This is a public endpoint that returns market-wide funding rate information without requiring authentication. ### Request #### Request Format ```json { "id": "funding-1", "method": "post", "params": { "action": "getFundingRate", "symbol": "BTC-USDT" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getFundingRate"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Normalized to canonical uppercase form — lowercase input is accepted and normalized automatically. | ### Response #### Success Response ```json { "id": "funding-1", "status": 200, "result": { "response": { "symbol": "BTC-USDT", "estimatedFundingRate": "0.000010960225996", "lastSettlementRate": "0.0001", "lastSettlementTime": 1704066000000, "nextFundingTime": 1704067200000, "fundingInterval": 3600000 }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | -------------------------------------- | ------- | --------------------------------------------------------------------------- | | `result.response.symbol` | string | Trading pair symbol (e.g., `"BTC-USDT"`) | | `result.response.estimatedFundingRate` | string | Estimated funding rate for the next settlement period as a decimal string | | `result.response.lastSettlementRate` | string | The actual funding rate from the most recent settlement as a decimal string | | `result.response.lastSettlementTime` | integer | Timestamp of the last funding settlement in milliseconds since epoch | | `result.response.nextFundingTime` | integer | Next funding time in milliseconds since epoch | | `result.response.fundingInterval` | integer | Funding interval in milliseconds (e.g., `3600000` for 1 hour) | | `result.status` | string | Always `"success"` for successful responses | **Note on `lastSettlementTime`**: For newly listed markets that have not yet had a funding settlement, `lastSettlementTime` is returned as `0`. Check for `0` before interpreting this value as a real settlement timestamp. #### Error Response ```json { "id": "funding-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid symbol" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request funding rate for BTC-USDT ws.send(JSON.stringify({ id: 'funding-1', method: 'post', params: { action: 'getFundingRate', symbol: 'BTC-USDT' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'funding-1' && response.status === 200) { const data = response.result.response; console.log('Funding Rate:', data); console.log(`Estimated Rate: ${data.estimatedFundingRate}`); console.log(`Last Settlement Rate: ${data.lastSettlementRate}`); console.log(`Last Settlement: ${new Date(data.lastSettlementTime)}`); console.log(`Next Funding: ${new Date(data.nextFundingTime)}`); } }; ``` ### Examples #### Get Funding Rate for BTC ```json { "id": "btc-funding", "method": "post", "params": { "action": "getFundingRate", "symbol": "BTC-USDT" } } ``` #### Get Funding Rate for ETH ```json { "id": "eth-funding", "method": "post", "params": { "action": "getFundingRate", "symbol": "ETH-USDT" } } ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Symbol Required**: A valid market symbol must be provided * **Funding Periods**: Funding rates settle every 1 hour (`fundingInterval: 3600000` ms) * **Real-time Updates**: For continuous funding rate updates, consider polling or using subscriptions * **Precision**: Funding rates are returned as decimal strings for precision ### Use Cases #### Risk Management * Monitor funding costs for open positions * Calculate funding payment projections * Assess carry costs for long-term positions #### Trading Strategy * Identify funding rate arbitrage opportunities * Adjust position sizes based on funding costs * Time entries/exits around funding periods #### Market Analysis * Track funding rate trends * Compare funding rates across markets * Analyze market sentiment through funding data ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **Market-Specific**: Request only the markets you need * **Caching**: Funding rates change infrequently, suitable for short-term caching ### Validation Rules * No authentication required (public Info WebSocket) * Symbol parameter is required and must be a valid market symbol * Invalid symbols will return a 400 error ### Related Endpoints * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time price feeds * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Comprehensive market prices * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getFundingRate) - HTTP request for funding rate import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Is Whitelisted (WebSocket) Check if a wallet address is whitelisted for trading operations through the WebSocket connection. This is a public endpoint that returns whitelist status without requiring authentication. ### Request #### Request Format ```json { "id": "whitelist-1", "method": "post", "params": { "action": "getIsWhitelisted", "walletAddress": "0x1234567890123456789012345678901234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getIsWhitelisted"` | | `params.walletAddress` | string | Yes | Ethereum wallet address to check | ### Response #### Success Response ```json { "id": "whitelist-1", "status": 200, "result": { "response": true, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | ----------------- | ------- | ----------------------------------------------------------------------- | | `result.response` | boolean | `true` if wallet is whitelisted and can place orders, `false` otherwise | | `result.status` | string | Always `"success"` for successful responses | #### Error Response ```json { "id": "whitelist-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid wallet address format" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Check if wallet is whitelisted ws.send(JSON.stringify({ id: 'whitelist-1', method: 'post', params: { action: 'getIsWhitelisted', walletAddress: '0x1234567890123456789012345678901234567890' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'whitelist-1' && response.status === 200) { const isWhitelisted = response.result.response; console.log(`Wallet whitelisted: ${isWhitelisted}`); if (isWhitelisted) { console.log('✅ Wallet can place orders'); } else { console.log('❌ Wallet cannot place orders'); } } }; ``` ### Examples #### Check Whitelist Status ```json { "id": "check-access", "method": "post", "params": { "action": "getIsWhitelisted", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } } ``` #### Validate Before Trading ```javascript async function checkTradingAccess(walletAddress) { const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); return new Promise((resolve, reject) => { ws.onopen = () => { ws.send(JSON.stringify({ id: 'check-whitelist', method: 'post', params: { action: 'getIsWhitelisted', walletAddress: walletAddress } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'check-whitelist') { ws.close(); if (response.status === 200) { resolve(response.result.response); } else { reject(new Error(response.error?.message || 'Unknown error')); } } }; ws.onerror = reject; }); } // Usage try { const canTrade = await checkTradingAccess('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); if (canTrade) { // Proceed with trading operations console.log('Access granted - can place orders'); } else { // Show whitelist message to user console.log('Access denied - wallet not whitelisted'); } } catch (error) { console.error('Failed to check whitelist status:', error); } ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Wallet Address Required**: Must provide a valid Ethereum address * **Boolean Response**: Returns simple true/false value * **Case Insensitive**: Wallet addresses are normalized (case doesn't matter) * **Real-time**: Reflects current whitelist status * **Access Control**: Used to enforce trading restrictions ### Use Cases #### Pre-Trade Validation * Check access before attempting to place orders * Show appropriate UI based on whitelist status * Provide feedback to users about trading access #### Access Management * Verify wallet eligibility for trading * Implement gated trading features * Enforce compliance requirements #### User Experience * Display whitelist status in UI * Show informative messages to non-whitelisted users * Guide users through whitelist process ### Whitelist System #### What is Whitelisting? The whitelist system controls which wallets can place orders on the platform. This may be used for: * Regulatory compliance * Phased rollouts * Access control during testing * Risk management #### Whitelist Status | Status | Can Place Orders | Can View Markets | Can Query Data | | ------------------- | ---------------- | ---------------- | -------------- | | **Whitelisted** | ✅ Yes | ✅ Yes | ✅ Yes | | **Not Whitelisted** | ❌ No | ✅ Yes | ✅ Yes | ### Address Format Validation #### Valid Formats ```javascript // Checksummed address (preferred) "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" // Lowercase "0x742d35cc6634c0532925a3b844bc9e7595f0beb" // Uppercase "0x742D35CC6634C0532925A3B844BC9E7595F0BEB" ``` #### Invalid Formats ```javascript // Missing 0x prefix "742d35Cc6634C0532925a3b844Bc9e7595f0bEb" // Too short "0x742d35Cc6634C0532925a3b844Bc9e7595" // Invalid characters "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEg" ``` ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple checks * **Lightweight**: Returns only boolean value (minimal data transfer) * **Efficient**: Fast whitelist lookup from arbitrator service ### Validation Rules * No authentication required (public Info WebSocket) * Wallet address parameter is required * `walletAddress` must not exceed 42 characters * Address must be a valid Ethereum hex address (0x + 40 hex characters) * Invalid addresses will return a 400 error ### Error Handling | Error | Cause | Solution | | ------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | | `Invalid request body` | Missing walletAddress parameter | Include walletAddress in params | | `walletAddress exceeds maximum length of 42 characters` | Address string longer than 42 characters | Verify address is a standard 42-character Ethereum address | | `Invalid wallet address format` | Address not a valid Ethereum address | Verify address format (0x + 40 hex chars) | | `whitelist arbitrator unavailable` | System error | Retry request or contact support | ### Integration Example ```javascript class TradingClient { async canPlaceOrders(walletAddress) { const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); return new Promise((resolve, reject) => { ws.onopen = () => { ws.send(JSON.stringify({ id: 'whitelist-check', method: 'post', params: { action: 'getIsWhitelisted', walletAddress: walletAddress } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'whitelist-check') { ws.close(); resolve(response.status === 200 && response.result.response === true); } }; ws.onerror = () => { ws.close(); reject(new Error('WebSocket error')); }; }); } } // Usage const client = new TradingClient(); const isAllowed = await client.canPlaceOrders('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'); ``` ### Related Endpoints * [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Place trading orders (requires whitelist) * [Get SubAccount IDs](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getSubAccountIds) - List wallet's subaccounts import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Last Trades (WebSocket) Retrieve the most recent trade execution history (fills) from Synthetix's orderbook across all users through the WebSocket connection. Each trade represents a filled order or partial fill that has been executed. This endpoint provides public market data without requiring authentication. ### Request #### Request Format ```json { "id": "trades-1", "method": "post", "params": { "action": "getLastTrades", "symbol": "BTC-USDT", "limit": 100 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getLastTrades"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Normalized to uppercase; maximum 20 characters. | | `params.limit` | integer | No | Maximum number of trades to return (default: 50, min: 1, max: 100) | ### Response #### Success Response ```json { "id": "trades-1", "status": 200, "result": { "response": { "trades": [ { "tradeId": "123456789", "symbol": "BTC-USDT", "side": "buy", "price": "50000.50", "quantity": "0.1", "timestamp": 1704067200500, "isMaker": false }, { "tradeId": "123456788", "symbol": "BTC-USDT", "side": "sell", "price": "50000.25", "quantity": "0.05", "timestamp": 1704067199800, "isMaker": true } ] }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | ------------------------ | ------ | ------------------------------------------- | | `result.response.trades` | array | Array of trade objects (see below) | | `result.status` | string | Always `"success"` for successful responses | #### Trade Object (Public) | Field | Type | Description | | ----------- | ------- | ------------------------------------------------ | | `tradeId` | string | Unique identifier for the trade | | `symbol` | string | Market symbol (e.g., `"BTC-USDT"`) | | `side` | string | Order side: `"buy"` or `"sell"` | | `price` | string | Execution price as string | | `quantity` | string | Executed quantity as string | | `timestamp` | integer | Execution time in milliseconds since epoch | | `isMaker` | boolean | Whether this was a maker order (added liquidity) | #### Error Response ```json { "id": "trades-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid symbol" } } ``` | Error | Cause | Resolution | | --------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request recent trades for BTC-USDT ws.send(JSON.stringify({ id: 'trades-1', method: 'post', params: { action: 'getLastTrades', symbol: 'BTC-USDT', limit: 50 } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'trades-1' && response.status === 200) { const data = response.result.response; console.log('Last Trades:', data); // Process trade data data.trades.forEach(trade => { console.log(`${trade.side} ${trade.quantity} @ ${trade.price}`); }); } }; ``` ### Examples #### Get Recent Trades with Custom Limit ```json { "id": "trades-custom", "method": "post", "params": { "action": "getLastTrades", "symbol": "BTC-USDT", "limit": 25 } } ``` #### Filter by Symbol ```json { "id": "trades-eth", "method": "post", "params": { "action": "getLastTrades", "symbol": "ETH-USDT", "limit": 100 } } ``` #### Maximum Results ```json { "id": "trades-max", "method": "post", "params": { "action": "getLastTrades", "symbol": "BTC-USDT", "limit": 100 } } ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Symbol Required**: A valid market symbol must be provided * **Limit Usage**: Default limit is 50 trades, minimum 1, maximum 100 trades per request * **Sorting**: Trades are returned in descending order by timestamp (newest first) * **Caching**: Trade data is immutable once created, suitable for short-term caching * **Real-time Updates**: For continuous updates, consider using WebSocket subscriptions for live trade streams ### Use Cases #### Market Analysis * Recent price discovery and trade flow analysis * Volume and liquidity assessment * Market depth analysis when combined with orderbook data #### Trading Interfaces * Displaying recent market activity * Trade history components * Price movement indicators #### Data Feeds * Building market data aggregators * Feeding external analytics systems * Creating public market displays ### Differences from getTrades | Feature | getLastTrades (Public) | getTrades (Private) | | -------------------- | ---------------------- | ------------------------------- | | **Authentication** | Not required | Required (EIP-712 signature) | | **Data Scope** | All users | Specific subaccount only | | **Sensitive Data** | Excluded | Included (fees, PnL, order IDs) | | **Symbol Parameter** | Required | Optional | | **Maximum Limit** | 100 trades | 1000 trades | ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **Efficient Updates**: Combine with orderbook subscriptions for complete market view * **Bandwidth**: Filter by symbol to reduce data transfer ### Validation Rules * No authentication required (public Info WebSocket) * No rate limiting beyond standard API limits * Symbol parameter is required, must be a valid market symbol, and is normalized to uppercase (maximum 20 characters) * Limit must be between 1 and 100 inclusive (defaults to 50 if not specified) ### Related Endpoints * [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) - Real-time orderbook changes * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Live price feeds * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getLastTrades) - HTTP request for last trades import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; import MarketPriceObject from '../../../../../snippets/market-price-object.mdx'; ## Get Market Prices (WebSocket) Retrieve current market prices and 24-hour statistics for all trading pairs through the WebSocket connection. This endpoint provides comprehensive real-time pricing data without requiring authentication. ### Request #### Request Format ```json { "id": "market-prices-1", "method": "post", "params": { "action": "getMarketPrices" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getMarketPrices"` | ### Response #### Success Response ```json { "id": "market-prices-1", "status": 200, "result": { "response": { "BTC-USDT": { "symbol": "BTC-USDT", "markPrice": "45025.37500000", "indexPrice": "45025.50000000", "lastPrice": "45025.00000000", "bestBid": "45020.00000000", "bestAsk": "45030.00000000", "volume24h": "1250.50", "quoteVolume24h": "56282500.00", "fundingRate": "0.00001250", "openInterest": "12500.75", "prevDayPrice": "44952.00000000", "timestamp": 1704067200000 }, "ETH-USDT": { "symbol": "ETH-USDT", "markPrice": "2998.75000000", "indexPrice": "2998.80000000", "lastPrice": "2998.75000000", "bestBid": "2996.00000000", "bestAsk": "3001.50000000", "volume24h": "8945.25", "quoteVolume24h": "26800000.00", "fundingRate": "0.00001250", "openInterest": "45678.50", "prevDayPrice": "2804.00000000", "timestamp": 1704067200000 } }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | ----------------- | ------ | ------------------------------------------- | | `result.response` | object | Map of market symbols to their price data | | `result.status` | string | Always `"success"` for successful responses | The response returns an object (map) where keys are market symbols and values are market price data: * **Object Format**: Keys are market symbols (e.g., "BTC-USDT"), enabling O(1) lookups * **Combined Data**: Each value includes real-time prices and rolling 24-hour statistics * **Symbol Included**: Each object contains the symbol field for consistency * **Fast Access**: Direct access to specific market data without iteration #### Error Response ```json { "id": "market-prices-1", "status": 500, "result": null, "error": { "code": 500, "message": "Could not get market prices" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request all market prices ws.send(JSON.stringify({ id: 'market-prices-1', method: 'post', params: { action: 'getMarketPrices' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'market-prices-1' && response.status === 200) { const markets = response.result.response; // Access specific market const btc = markets['BTC-USDT']; console.log(`BTC Mark Price: ${btc.markPrice}`); console.log(`BTC 24h Volume: ${btc.volume24h}`); // Iterate all markets Object.values(markets).forEach(market => { console.log(`${market.symbol}: ${market.lastPrice}`); }); } }; ``` ### Examples #### Get All Market Prices ```json { "id": "all-prices", "method": "post", "params": { "action": "getMarketPrices" } } ``` #### Processing Response Data ```javascript // Access specific market directly by symbol const btcPrice = response.result.response['BTC-USDT'].markPrice; // Calculate 24h price change const market = response.result.response['ETH-USDT']; const priceChange = parseFloat(market.lastPrice) - parseFloat(market.prevDayPrice); const priceChangePercent = (priceChange / parseFloat(market.prevDayPrice)) * 100; console.log(`ETH 24h Change: ${priceChangePercent.toFixed(2)}%`); // Find highest volume market const markets = Object.values(response.result.response); const highestVolume = markets.reduce((max, m) => parseFloat(m.quoteVolume24h) > parseFloat(max.quoteVolume24h) ? m : max ); console.log(`Highest Volume: ${highestVolume.symbol}`); ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **All Markets**: Returns data for all available trading pairs in a single request * **Real-time Prices**: Mark, index, and last prices updated continuously from price feeds * **Rolling Statistics**: 24-hour volume and price change calculated on a rolling window * **Object Format**: Response uses symbol keys for efficient O(1) lookups ### Data Fields Explained #### Price Types ##### Mark Price The mark price is the current fair value of the perpetual contract used for: * Position valuation in portfolios * Liquidation price calculations * PnL calculations * Updated continuously based on trading activity and price feeds ##### Index Price The index price is a reference price derived from external spot exchanges: * Provides an external price reference independent of Synthetix trading * Used as a baseline for mark price calculations * Sourced from multiple spot exchanges for accuracy ##### Prev Day Price The market (last) price 24 hours ago, used for calculating 24-hour price changes. #### Core Metrics ##### Volume * **volume24h**: Total base asset traded in 24 hours (e.g., BTC amount) * **quoteVolume24h**: Total quote asset traded in 24 hours (e.g., USDT amount) * Rolling 24-hour window, continuously updated ##### Funding Rate * **fundingRate**: Current estimated funding rate for the market, fetched from the funding rate service * For per-symbol funding rate history, see [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getFundingRate) ##### Open Interest * **openInterest**: Total open positions in base asset * Key indicator of market activity and liquidity ### Use Cases #### Trading Dashboards * Display real-time prices for all markets * Show 24-hour price changes and volume * Monitor bid/ask spreads across markets #### Market Screening * Compare volume and open interest across markets * Identify most active trading pairs * Track market-wide price movements #### Portfolio Valuation * Mark-to-market position values using mark price * Calculate unrealized PnL * Monitor price movements affecting open positions #### Risk Monitoring * Track price volatility across markets * Monitor open interest changes * Assess market liquidity via bid/ask spreads ### Validation Rules * No authentication required (public Info WebSocket) * No parameters to validate beyond action name * Returns data for all available markets * Real-time data from price feeds ### Common Errors | Error | Description | | -------------------------------- | ----------------------------------- | | Market configuration unavailable | Failed to retrieve market list | | Mark price unavailable | Failed to retrieve mark price data | | Index price unavailable | Failed to retrieve index price data | | Could not get market prices | Internal service error | ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **Single Request**: Get all market data in one request instead of per-market queries * **Efficient Updates**: For continuous updates, use [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) subscription ### Related Endpoints * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time streaming of price updates * [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) - Market configuration and trading rules * [Get Mid Prices](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMids) - Simple mid prices for all markets * [Get Funding Rate](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getFundingRate) - Current funding rates * [Get Open Interest](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getOpenInterest) - Open interest data * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - HTTP request for market prices import MarketObject from '../../../../../snippets/market-object.mdx'; import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Markets (WebSocket) Retrieve comprehensive market configuration information for all available trading pairs through the WebSocket connection. ### Request #### Request Format ```json { "id": "markets-1", "method": "post", "params": { "action": "getMarkets" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getMarkets"` | ### Response #### Success Response ```json { "id": "markets-1", "status": 200, "result": { "response": [ { "symbol": "SOL-USDT", "description": "Solana", "baseAsset": "SOL", "quoteAsset": "USDT", "isOpen": true, "isCloseOnly": false, "priceExponent": 2, "quantityExponent": 2, "priceIncrement": "0.01", "minOrderSize": "0.01", "orderSizeIncrement": "0.01", "contractSize": 1, "maxMarketOrderSize": "1000000", "maxLimitOrderSize": "1000000", "minOrderPrice": "0.01", "limitOrderPriceCapRatio": "1.5", "limitOrderPriceFloorRatio": "0.5", "marketOrderPriceCapRatio": "1.1", "marketOrderPriceFloorRatio": "0.9", "liquidationClearanceFee": "0.002", "minNotionalValue": "10", "maintenanceMarginTiers": [ { "minPositionSize": "0", "maxPositionSize": "500000", "maxLeverage": 100, "initialMarginRequirement": "0.01", "maintenanceMarginRequirement": "0.005", "maintenanceDeductionValue": "0" }, { "minPositionSize": "500001", "maxPositionSize": "10000000", "maxLeverage": 50, "initialMarginRequirement": "0.02", "maintenanceMarginRequirement": "0.01", "maintenanceDeductionValue": "0" }, { "minPositionSize": "10000001", "maxPositionSize": "50000000", "maxLeverage": 25, "initialMarginRequirement": "0.04", "maintenanceMarginRequirement": "0.02", "maintenanceDeductionValue": "0" }, { "minPositionSize": "50000001", "maxPositionSize": "200000000", "maxLeverage": 10, "initialMarginRequirement": "0.1", "maintenanceMarginRequirement": "0.05", "maintenanceDeductionValue": "0" }, { "minPositionSize": "200000001", "maxPositionSize": "", "maxLeverage": 2, "initialMarginRequirement": "0.5", "maintenanceMarginRequirement": "0.25", "maintenanceDeductionValue": "0" } ] } ], "status": "success" } } ``` > **Note**: The last margin tier uses `"maxPositionSize": ""` (empty string), which indicates unlimited position size for positions above 200M USD notional. #### Error Response ```json { "id": "markets-1", "status": 500, "result": null, "error": { "code": 500, "message": "Could not pull market configuration" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request all markets ws.send(JSON.stringify({ id: 'markets-1', method: 'post', params: { action: 'getMarkets' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'markets-1' && response.status === 200) { const markets = response.result.response; console.log('Markets:', markets); console.log(`Total markets: ${markets.length}`); // Process market configuration markets.forEach(market => { console.log(`${market.symbol}: ${market.description}`); }); } }; ``` ### Market Information * **Comprehensive Data**: Includes all market configuration, trading rules, and limits * **Real-time Updates**: Market status and configuration are updated in real-time * **Trading Rules**: All necessary information for placing compliant orders * **Margin Requirements**: Complete margin tier information for risk management * **Price Precision**: Exact decimal precision for prices and quantities ### Use Cases * **Trading Applications**: Get market specifications for order validation * **Risk Management**: Access margin requirements and position limits * **Market Analysis**: Understand trading rules and market structure * **Order Management**: Validate order parameters before submission * **System Integration**: Configure trading systems with market rules ### Validation Rules * No authentication required (public Info WebSocket) * No rate limiting beyond standard API limits * Returns data for all available markets ### Related Endpoints * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket) - Current market prices via WebSocket * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - HTTP GET request for markets import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Mid Prices (WebSocket) Retrieve the current mid prices for all available markets through the WebSocket connection. Mid prices represent the midpoint between the best bid and best ask prices, providing a simple reference price for each market. ### Request #### Request Format ```json { "id": "mids-1", "method": "post", "params": { "action": "getMids" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getMids"` | ### Response #### Success Response ```json { "id": "mids-1", "status": 200, "result": { "response": { "BTC-USDT": "45025.37500000", "ETH-USDT": "2845.12500000", "SOL-USDT": "98.75000000", "AVAX-USDT": "35.50000000" }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | -------------------------- | ------ | ------------------------------------------------------------- | | `result.response.{symbol}` | string | Mid price for the specified market symbol as a decimal string | | `result.status` | string | Always `"success"` for successful responses | #### Error Response ```json { "id": "mids-1", "status": 500, "result": null, "error": { "code": 500, "message": "Could not pull market configuration" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request mid prices for all markets ws.send(JSON.stringify({ id: 'mids-1', method: 'post', params: { action: 'getMids' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'mids-1' && response.status === 200) { const midPrices = response.result.response; console.log('Mid Prices:', midPrices); // Process mid prices Object.entries(midPrices).forEach(([symbol, price]) => { console.log(`${symbol}: $${price}`); }); } }; ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **All Markets**: Returns mid prices for all available markets in a single request * **No Filtering**: Cannot filter by symbol - all markets are always returned * **Mid Price Calculation**: Calculated from the latest price feed data * **Precision**: Prices are returned as decimal strings to preserve precision * **Efficient**: Single request returns all market mid prices ### Use Cases #### Price Monitoring * Quick overview of all market prices * Price comparison across markets * Market snapshot for dashboards #### Trading Interfaces * Display current market prices * Calculate position values * Show market overview #### Data Feeds * Simple price aggregation * Price feed for external systems * Market status displays ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **All Markets**: Single request returns all markets (no need for multiple calls) * **Real-time Updates**: For continuous updates, use [`marketPriceUpdates`](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) subscription ### Comparison with Other Price Endpoints | Feature | getMids | getMarketPrices | | ----------------- | --------------------- | ---------------------------------------- | | **Data Returned** | Mid price only | Mark, index, best bid/ask, mid, and more | | **Markets** | All markets | Can filter by symbols | | **Response Size** | Minimal | Comprehensive | | **Use Case** | Quick price reference | Detailed price data | ### Validation Rules * No authentication required (public Info WebSocket) * No parameters required beyond the action * Returns all available markets ### Related Endpoints * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket) - Comprehensive price data via WebSocket * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time price streams * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMids) - HTTP request for mid prices import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; import OpenInterestObject from '../../../../../snippets/open-interest-object.mdx'; ## Get Open Interest (WebSocket) Retrieve open interest data for all trading markets through the WebSocket connection. Open interest represents the total number of outstanding derivative contracts that have not been settled. ### Request #### Request Format ```json { "id": "oi-1", "method": "post", "params": { "action": "getOpenInterest" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getOpenInterest"` | ### Response #### Success Response ```json { "id": "oi-1", "status": 200, "result": { "response": [ { "symbol": "BTC-USDT", "openInterest": "1250.50", "longOpenInterest": "750.25", "shortOpenInterest": "500.25", "timestamp": 1704067200000 }, { "symbol": "ETH-USDT", "openInterest": "8500.75", "longOpenInterest": "4800.50", "shortOpenInterest": "3700.25", "timestamp": 1704067200000 }, { "symbol": "SOL-USDT", "openInterest": "45000.00", "longOpenInterest": "25000.00", "shortOpenInterest": "20000.00", "timestamp": 1704067200000 } ], "status": "success" } } ``` #### Error Response ```json { "id": "oi-1", "status": 500, "result": null, "error": { "code": 500, "message": "Failed to retrieve open interest data" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request open interest for all markets ws.send(JSON.stringify({ id: 'oi-1', method: 'post', params: { action: 'getOpenInterest' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'oi-1' && response.status === 200) { const openInterestData = response.result.response; console.log('Open Interest Data:', openInterestData); // Process open interest openInterestData.forEach(market => { console.log(`${market.symbol}:`); console.log(` Total OI: ${market.openInterest}`); console.log(` Long: ${market.longOpenInterest}`); console.log(` Short: ${market.shortOpenInterest}`); }); } }; ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **All Markets**: Returns open interest for all available markets in a single request * **No Filtering**: Cannot filter by symbol - all markets are always returned * **Precision**: Values are returned as decimal strings to preserve precision * **Real-time Data**: Open interest updates as positions are opened/closed * **Capture Time**: `timestamp` (Unix ms) reflects when each market's open interest was last captured by the platform * **Long/Short Split**: Provides breakdown of long vs short open interest ### Use Cases #### Market Analysis * Monitor market sentiment through long/short ratio * Identify overcrowded trades * Track market liquidity * Analyze position imbalances #### Risk Management * Assess market exposure * Monitor systemic risk * Track position concentration * Evaluate market capacity #### Trading Strategy * Contrarian indicators from extreme long/short ratios * Liquidity assessment for large orders * Market depth analysis * Position sizing based on open interest ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **All Markets**: Single request returns all markets (no need for multiple calls) * **Efficient**: Minimal payload for quick data retrieval ### Understanding Open Interest #### Key Metrics | Metric | Description | | ----------------------- | ------------------------------------------------------ | | **Total Open Interest** | Sum of all outstanding long and short positions | | **Long Open Interest** | Total size of all long positions | | **Short Open Interest** | Total size of all short positions | | **Long/Short Ratio** | Ratio of long to short positions (sentiment indicator) | #### Interpretation * **Rising OI + Rising Price**: Strong bullish trend (new longs entering) * **Rising OI + Falling Price**: Strong bearish trend (new shorts entering) * **Falling OI + Rising Price**: Short covering rally (weak bullish) * **Falling OI + Falling Price**: Long liquidations (weak bearish) ### Validation Rules * No authentication required (public Info WebSocket) * No parameters required beyond the action * Returns all available markets ### Related Endpoints * [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) - Market configuration * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time price data * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOpenInterest) - HTTP request for open interest import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get Orderbook (WebSocket) Retrieve order book depth (market depth) for a specific trading pair through the WebSocket connection. This endpoint provides a real-time snapshot of buy and sell orders without requiring authentication. ### Request #### Request Format ```json { "id": "orderbook-1", "method": "post", "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 100 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getOrderbook"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`); max 20 characters; normalized to uppercase | | `params.limit` | integer | No | Number of orders to return per side (default: 500) | #### Valid Limits | Limit | Description | | ------ | ------------------------ | | `5` | Top 5 orders per side | | `10` | Top 10 orders per side | | `20` | Top 20 orders per side | | `50` | Top 50 orders per side | | `100` | Top 100 orders per side | | `500` | Top 500 orders per side | | `1000` | Top 1000 orders per side | ### Response #### Success Response ```json { "id": "orderbook-1", "status": 200, "result": { "response": { "bids": [ ["45000.00", "1.5"], ["44999.50", "2.0"], ["44999.00", "0.8"] ], "asks": [ ["45001.00", "1.2"], ["45001.50", "3.1"], ["45002.00", "0.9"] ] }, "status": "success" } } ``` #### Response Fields | Field | Type | Description | | ---------------------- | ------ | ------------------------------------------------------------------ | | `result.response.bids` | array | Array of bid orders \[price, quantity], sorted by price descending | | `result.response.asks` | array | Array of ask orders \[price, quantity], sorted by price ascending | | `result.status` | string | Always `"success"` for successful responses | #### Order Array Format Each order in the `bids` and `asks` arrays is represented as: ```json ["price", "quantity"] ``` Where: * `price` (string): Order price level * `quantity` (string): Total quantity available at this price level #### Error Response ```json { "id": "orderbook-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid limit parameter" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request orderbook for BTC-USDT ws.send(JSON.stringify({ id: 'orderbook-1', method: 'post', params: { action: 'getOrderbook', symbol: 'BTC-USDT', limit: 100 } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'orderbook-1' && response.status === 200) { const { bids, asks } = response.result.response; // Best bid and ask const bestBid = bids[0]; const bestAsk = asks[0]; console.log(`Best Bid: ${bestBid[0]} @ ${bestBid[1]}`); console.log(`Best Ask: ${bestAsk[0]} @ ${bestAsk[1]}`); // Calculate spread const spread = parseFloat(bestAsk[0]) - parseFloat(bestBid[0]); console.log(`Spread: ${spread.toFixed(2)}`); // Calculate total liquidity const totalBidLiquidity = bids.reduce((sum, [_, qty]) => sum + parseFloat(qty), 0); const totalAskLiquidity = asks.reduce((sum, [_, qty]) => sum + parseFloat(qty), 0); console.log(`Bid Liquidity: ${totalBidLiquidity.toFixed(4)}`); console.log(`Ask Liquidity: ${totalAskLiquidity.toFixed(4)}`); } }; ``` ### Examples #### Get Top 100 Orders ```json { "id": "orderbook-100", "method": "post", "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 100 } } ``` #### Get Top 20 Orders (Lightweight) ```json { "id": "orderbook-20", "method": "post", "params": { "action": "getOrderbook", "symbol": "ETH-USDT", "limit": 20 } } ``` #### Get Full Depth (1000 Levels) ```json { "id": "orderbook-full", "method": "post", "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 1000 } } ``` #### Get Minimal Depth (Top 5) ```json { "id": "orderbook-minimal", "method": "post", "params": { "action": "getOrderbook", "symbol": "BTC-USDT", "limit": 5 } } ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Symbol Required**: A valid market symbol must be provided * **Aggregated Levels**: Orders at the same price are aggregated into a single level * **Snapshot Data**: Returns a point-in-time snapshot of the order book * **Sorted Orders**: Bids sorted descending (highest first), asks sorted ascending (lowest first) ### Data Format Notes * **Price Precision**: Prices use market-specific decimal precision * **Quantity Precision**: Quantities use market-specific decimal precision * **String Format**: Both price and quantity are returned as strings for precision * **Snapshot Consistency**: All data from the same timestamp ### Use Cases #### Market Analysis * Understand liquidity distribution across price levels * Identify support and resistance levels * Analyze order book imbalance #### Trading Decisions * Evaluate slippage for planned order sizes * Assess market depth before placing large orders * Monitor bid/ask spread changes #### Price Discovery * See where orders are concentrated * Identify potential price movement barriers * Track large order placements #### Liquidity Assessment * Evaluate market depth for institutional orders * Calculate available liquidity at price ranges * Compare liquidity across trading pairs ### Validation Rules * No authentication required (public Info WebSocket) * Symbol parameter is required and must be a valid market symbol; max 20 characters; normalized to uppercase before lookup * Limit must be one of the valid values: 5, 10, 20, 50, 100, 500, 1000 * Default limit is 500 if not specified ### Common Errors | Error | Description | | ----------------------- | -------------------------------------------------------- | | Symbol required | Missing trading pair symbol | | Invalid limit | Limit not in valid range (5, 10, 20, 50, 100, 500, 1000) | | Invalid symbol | Symbol does not exist | | Timeout | Request to matching service timed out | | Market data unavailable | Failed to retrieve order book data | ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple requests * **Choose Appropriate Depth**: Use smaller limits (5, 10, 20) for faster responses when full depth isn't needed * **Real-time Updates**: For continuous updates, use [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) subscription ### Related Endpoints * [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) - Real-time orderbook changes via subscription * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarketPrices) - Current market prices including best bid/ask * [Get Last Trades](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getLastTrades) - Recent trade executions * [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) - Market configuration and precision * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) - HTTP request for orderbook import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Get SubAccount IDs (WebSocket) Returns all subaccount IDs associated with a wallet address through the WebSocket connection. This is a public endpoint that allows looking up subaccounts without authentication. ### Request #### Request Format ```json { "id": "subaccounts-1", "method": "post", "params": { "action": "getSubAccountIds", "walletAddress": "0x1234567890123456789012345678901234567890" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request parameters wrapper | | `params.action` | string | Yes | Must be `"getSubAccountIds"` | | `params.walletAddress` | string | Yes | Ethereum wallet address to query. Must use EIP-55 checksum casing — non-checksummed addresses return a 400 error with the expected checksummed form. | | `params.includeDelegations` | boolean | No | When `true`, returns owned and delegated subaccount IDs as separate fields. Defaults to `false` (flat array of owned IDs). | ### Response #### Success Response (default) When `includeDelegations` is omitted or `false`, the response is a flat array of owned subaccount IDs: ```json { "id": "subaccounts-1", "status": 200, "result": { "response": [ "1867542890123456789", "1867542890123456790", "1867542890123456791" ], "status": "success" } } ``` #### Success Response (with `includeDelegations: true`) When `includeDelegations` is `true`, `result.response` is an object with owned and delegated IDs listed separately: ```json { "id": "subaccounts-1", "status": 200, "result": { "response": { "subAccountIds": [ "1867542890123456789", "1867542890123456790" ], "delegatedSubAccountIds": [ "1867542890123456800" ] }, "status": "success" } } ``` #### Response Fields **Default response** (`includeDelegations` omitted or `false`): | Field | Type | Description | | ----------------- | --------- | ---------------------------------------------------------------------- | | `result.response` | string\[] | Array of owned subaccount ID strings (uint64 as strings for precision) | | `result.status` | string | Always `"success"` for successful responses | **With `includeDelegations: true`**: | Field | Type | Description | | ---------------------------------------- | --------- | ------------------------------------------------------- | | `result.response.subAccountIds` | string\[] | Subaccount IDs owned by the wallet address | | `result.response.delegatedSubAccountIds` | string\[] | Subaccount IDs for which the wallet has delegate access | | `result.status` | string | Always `"success"` for successful responses | #### Error Response Invalid address format: ```json { "id": "subaccounts-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid wallet address format" } } ``` EIP-55 checksum mismatch: ```json { "id": "subaccounts-1", "status": 400, "result": null, "error": { "code": 400, "message": "walletAddress must use EIP-55 checksum casing; expected 0x742D35CC6634c0532925A3b844BC9E7595F0BEb0" } } ``` ### WebSocket Connection Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request subaccount IDs for a wallet ws.send(JSON.stringify({ id: 'subaccounts-1', method: 'post', params: { action: 'getSubAccountIds', walletAddress: '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'subaccounts-1' && response.status === 200) { const subaccountIds = response.result.response; console.log('Subaccount IDs:', subaccountIds); console.log(`Found ${subaccountIds.length} subaccounts`); subaccountIds.forEach((id, index) => { console.log(` ${index + 1}. ${id}`); }); } }; ``` ### Examples #### Lookup Subaccounts for Wallet ```json { "id": "lookup-1", "method": "post", "params": { "action": "getSubAccountIds", "walletAddress": "0x1234567890123456789012345678901234567890" } } ``` #### Lookup Owned and Delegated Subaccounts ```json { "id": "lookup-delegated-1", "method": "post", "params": { "action": "getSubAccountIds", "walletAddress": "0x742D35CC6634c0532925A3b844BC9E7595F0BEb0", "includeDelegations": true } } ``` #### Check if Wallet Has Subaccounts ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { ws.send(JSON.stringify({ id: 'check-subaccounts', method: 'post', params: { action: 'getSubAccountIds', walletAddress: '0x742D35CC6634c0532925A3b844BC9E7595F0BEb0' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'check-subaccounts' && response.status === 200) { const subaccountIds = response.result.response; const hasSubaccounts = subaccountIds.length > 0; console.log(`Wallet has subaccounts: ${hasSubaccounts}`); if (hasSubaccounts) { console.log(`Total subaccounts: ${subaccountIds.length}`); } } }; ``` ### Implementation Notes * **Public Data**: No authentication required for this endpoint * **Wallet Address Required**: Must provide a valid Ethereum address * **All Subaccounts**: Returns all subaccounts ever created for the wallet address * **ID Format**: Subaccount IDs are returned as strings to preserve uint64 precision * **EIP-55 Required**: `walletAddress` must use EIP-55 checksum casing. Lowercase, uppercase, or any other non-checksummed form returns a 400 error that includes the expected checksummed address. * **Historical Data**: Includes all subaccounts, even if no longer active * **Delegation Lookup**: When `includeDelegations: true`, a parallel lookup for delegated subaccounts is performed ### Use Cases #### Account Discovery * Find all subaccounts associated with a wallet * Verify subaccount ownership * List available trading accounts #### Delegation-Aware Lookups * Enumerate all subaccounts a delegate address can act on * Build interfaces that surface both owned and delegated accounts #### Integration Support * Allow users to select from existing subaccounts * Validate subaccount IDs before operations * Display account lists in trading interfaces #### Account Management * Check if wallet has trading accounts * Enumerate subaccounts for migration * Audit subaccount creation ### Address Format Validation #### Valid Formats ```javascript // EIP-55 checksummed address (required) "0x742D35CC6634c0532925A3b844BC9E7595F0BEb0" ``` #### Invalid Formats ```javascript // Lowercase — rejected with EIP-55 checksum error "0x742d35cc6634c0532925a3b844bc9e7595f0beb0" // Uppercase — rejected with EIP-55 checksum error "0x742D35CC6634C0532925A3B844BC9E7595F0BEB0" // Missing 0x prefix "742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" // Too short "0x742d35Cc6634C0532925a3b844Bc9e7595" // Invalid characters "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEg0" ``` ### Performance Considerations * **Low Latency**: WebSocket connection provides faster response times than REST * **Persistent Connection**: Reuse the same connection for multiple lookups * **Lightweight**: Returns only subaccount IDs (minimal data transfer) * **Efficient Lookup**: Fast database query for subaccount enumeration ### Validation Rules * No authentication required (public Info WebSocket) * Wallet address parameter is required * Address must be a valid Ethereum hex address (0x + 40 hex characters) * Address must use EIP-55 checksum casing — non-checksummed addresses are rejected * Invalid addresses will return a 400 error ### Error Handling | Error | Cause | Solution | | --------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `Invalid wallet address format` | Address is not a valid Ethereum hex address | Verify format: 0x prefix + 40 hex characters | | `walletAddress must use EIP-55 checksum casing; expected 0x...` | Address format is valid but casing does not match EIP-55 checksum | Use the checksummed address returned in the error message, or compute EIP-55 checksum before sending | | `Invalid request body` | Missing walletAddress parameter | Include walletAddress in params | ### Related Endpoints * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Detailed subaccount information (authenticated) * [Create Subaccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/createSubaccount) - Create new subaccount * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/info/getSubAccountIds) - HTTP request for subaccount IDs import WsInfoEndpoints from '../../../../../snippets/ws-info-endpoints.mdx'; ## Info WebSocket Public WebSocket endpoint for real-time market data and information queries without authentication. ### Overview The Info WebSocket endpoint supports two types of requests: 1. **Request/Response Pattern** - Query market data using `method: "post"` with action parameters 2. **Subscribe/Broadcast Pattern** - Subscribe to real-time data streams using `method: "subscribe"` No authentication required for any operations on this endpoint. ### Request/Response Pattern Use `method: "post"` with an action parameter to query market information. The server responds with the requested data. #### Available Actions | Action | Description | Documentation | | ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------- | | `getMarkets` | Retrieve all available markets and their configuration | [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) | | `getSubAccountIds` | Get all subaccount IDs for a wallet address | [Get SubAccount IDs](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getSubAccountIds) | #### Request Format ```json { "id": "request-1", "method": "post", "params": { "action": "getMarkets" } } ``` #### Response Format ```json { "id": "request-1", "status": 200, "result": { "response": [], "status": "success" } } ``` #### Example: Query Markets ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Request all markets ws.send(JSON.stringify({ id: 'markets-1', method: 'post', params: { action: 'getMarkets' } })); }; ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === 'markets-1' && response.status === 200) { const markets = response.result.response; console.log('Markets:', markets); console.log(`Found ${markets.length} markets`); } }; ``` ### Subscribe/Broadcast Pattern Subscribe to real-time data streams to receive continuous updates as events occur. The server broadcasts updates to all subscribed connections. #### Available Subscription Types | Type | Description | Required Parameters | Update Method | | -------------- | -------------------------------------------------- | --------------------- | ------------------------ | | `candles` | Real-time candlestick updates | `symbol`, `timeframe` | `candle_update` | | `marketPrices` | Live market price updates (mark, last, index, mid) | `symbol` | `market_price_update` | | `orderbook` | Orderbook depth updates | `symbol` | `orderbook_depth_update` | #### Subscribe Request ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT" } } ``` #### Subscribe Response ```json { "id": "sub-1", "status": 200, "result": { "message": "Subscribed to orderbook for BTC-USDT" } } ``` #### Unsubscribe Request ```json { "id": "unsub-1", "method": "unsubscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT" } } ``` #### Broadcast Updates After subscribing, the server sends updates using the notification format: ```json { "method": "orderbook_depth_update", "data": { "symbol": "BTC-USDT", "timestamp": "1735689600000", "bids": [ {"price": "50000.00", "quantity": "1.5"}, {"price": "49999.00", "quantity": "2.0"} ], "asks": [ {"price": "50001.00", "quantity": "1.2"}, {"price": "50002.00", "quantity": "1.8"} ] } } ``` ### Subscription Examples #### Orderbook Updates ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // Subscribe to BTC-USDT orderbook ws.send(JSON.stringify({ id: 'sub-orderbook', method: 'subscribe', params: { type: 'orderbook', symbol: 'BTC-USDT' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); // Handle subscription confirmation if (message.id === 'sub-orderbook') { console.log('Subscribed successfully:', message.result); } // Handle orderbook updates if (message.method === 'orderbook_depth_update') { console.log('Orderbook update:', message.data); } }; ``` #### Market Price Updates ```javascript ws.onopen = () => { // Subscribe to ETH-USDT price updates ws.send(JSON.stringify({ id: 'sub-prices', method: 'subscribe', params: { type: 'marketPrices', symbol: 'ETH-USDT' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); // Handle price updates if (message.method === 'market_price_update') { const { symbol, price, updateType, lastUpdateTime } = message.data; console.log(`${symbol} ${updateType} price: ${price} at ${lastUpdateTime}`); } }; ``` #### Candle Updates ```javascript ws.onopen = () => { // Subscribe to SOL-USDT 1-minute candles ws.send(JSON.stringify({ id: 'sub-candles', method: 'subscribe', params: { type: 'candles', symbol: 'SOL-USDT', timeframe: '1m' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); // Handle candle updates if (message.method === 'candle_update') { const candle = message.data; console.log(`Candle: O:${candle.open} H:${candle.high} L:${candle.low} C:${candle.close}`); } }; ``` ### Complete Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { console.log('Connected to Info WebSocket'); // 1. Query available markets (request/response) ws.send(JSON.stringify({ id: 'get-markets', method: 'post', params: { action: 'getMarkets' } })); // 2. Subscribe to BTC-USDT orderbook (subscription) ws.send(JSON.stringify({ id: 'sub-orderbook', method: 'subscribe', params: { type: 'orderbook', symbol: 'BTC-USDT' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); // Handle request/response if (message.id) { if (message.id === 'get-markets') { console.log('Available markets:', message.result); } if (message.id === 'sub-orderbook') { console.log('Subscription confirmed:', message.result); } } // Handle broadcast updates if (message.method) { switch (message.method) { case 'orderbook_depth_update': console.log('Orderbook update:', message.data); break; case 'market_price_update': console.log('Price update:', message.data); break; case 'candle_update': console.log('Candle update:', message.data); break; } } }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; ws.onclose = () => { console.log('WebSocket connection closed'); }; ``` ### Features * **No Authentication Required** - Public endpoint, no EIP-712 signatures needed * **Low-Latency Streaming** - Real-time market data with minimal delay * **Request/Response + Subscriptions** - Both query and streaming patterns supported * **Multiple Subscriptions** - Subscribe to multiple markets and data types simultaneously * **Efficient Updates** - Delta updates for orderbook, full snapshots on subscription ### Error Responses ```json { "id": "request-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid subscription type" } } ``` ### Next Steps * [Get Markets](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getMarkets) - Query all available markets * [Trade WebSocket](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket) - Authenticated trading operations * [WebSocket Overview](https://developers.synthetix.io/developer-resources/api/ws-api) - WebSocket API introduction * [REST API](https://developers.synthetix.io/developer-resources/api/rest-api) - REST API alternatives import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import CandleObject from '../../../../../snippets/candle-object.mdx'; ## Candle Updates Receive real-time OHLCV price candles for charting and technical analysis across multiple timeframes. Note: In other exchanges this is sometime called `candleSticks`. ### Subscription Request #### Subscribe to Candle Updates ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "candles", "symbol": "BTC-USDT", "timeframe": "1m" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"subscribe"` | | `params.type` | string | Yes | Must be `"candles"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., "BTC-USDT") | | `params.timeframe` | string | Yes | Candle timeframe (e.g., "1m", "5m", "1h", "1d") | ### Supported Intervals | Interval | Description | | -------- | ----------------- | | `1m` | 1-minute candles | | `3m` | 3-minute candles | | `5m` | 5-minute candles | | `15m` | 15-minute candles | | `30m` | 30-minute candles | | `1h` | 1-hour candles | | `2h` | 2-hour candles | | `4h` | 4-hour candles | | `6h` | 6-hour candles | | `8h` | 8-hour candles | | `12h` | 12-hour candles | | `1d` | 1-day candles | | `3d` | 3-day candles | | `1w` | 1-week candles | | `1M` | 1-month candles | | `3M` | 3-month candles | ### Candle Update Messages #### New Candle ```json { "channel": "candleUpdate", "data": { "symbol": "BTC-USDT", "timeframe": "1m", "open_price": "50000.00", "high_price": "50125.50", "low_price": "49875.00", "close_price": "50050.25", "volume": "12.5", "open_time": "2025-01-01T00:00:00Z", "close_time": "2025-01-01T00:00:59.999Z", "quote_volume": "625125.75", "trade_count": 145 }, "timestamp": 1735689600000 } ``` \:::warning Field Naming WebSocket candle updates use snake\_case field names (`open_price`, `close_time`, etc.) while REST API candles use camelCase. This is due to WebSocket using the internal NATS message format directly. \::: #### Candle Update (In Progress) Example of an in-progress candle (not yet closed): ```json { "channel": "candleUpdate", "data": { "symbol": "BTC-USDT", "timeframe": "1m", "open_price": "50050.25", "high_price": "50075.00", "low_price": "50025.00", "close_price": "50060.50", "volume": "3.2", "open_time": "2025-01-01T00:01:00Z", "close_time": "2025-01-01T00:01:59.999Z", "quote_volume": "160193.60", "trade_count": 42 }, "timestamp": 1735689660000 } ``` ### Candle Data Fields #### WebSocket Candle Update Fields \:::warning Snake Case WebSocket candle updates use snake\_case field names as shown in the examples above. \::: | Field | Type | Description | | -------------- | ------- | ----------------------------------------------- | | `symbol` | string | Trading pair symbol | | `timeframe` | string | Candle time interval (e.g., "1m", "5m", "1h") | | `open_time` | string | Candle open time (RFC3339 format) | | `close_time` | string | Candle close time (RFC3339 format) | | `open_price` | string | Opening price | | `high_price` | string | Highest price in the period | | `low_price` | string | Lowest price in the period | | `close_price` | string | Closing price (or current price if in progress) | | `volume` | string | Base asset volume | | `quote_volume` | string | Quote asset volume | | `trade_count` | integer | Number of trades in the period | **Note**: WebSocket candle updates include real-time in-progress candles. The `close_price` represents the current price until the candle period completes. ### Implementation Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { // No authentication needed - candles is public data const subscription = { id: "sub-1", method: "subscribe", params: { type: "candles", symbol: "BTC-USDT", timeframe: "1m" } }; ws.send(JSON.stringify(subscription)); }; // Handle candle update messages ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.channel === "candleUpdate") { // WebSocket candles use snake_case field names const { symbol, timeframe, open_price, high_price, low_price, close_price } = message.data; console.log(`${symbol} ${timeframe} candle: O=${open_price} H=${high_price} L=${low_price} C=${close_price}`); // Update chart with new candle data updateCandlestickChart(message.data); } }; function updateCandlestickChart(candleData) { // Destructure using actual snake_case field names const { open_time, // RFC3339 string open_price, high_price, low_price, close_price, volume } = candleData; // Parse timestamp from RFC3339 string const timestamp = new Date(open_time).getTime(); // Add/update candle in chart chart.updateCandle({ time: timestamp / 1000, // TradingView expects seconds open: parseFloat(open_price), high: parseFloat(high_price), low: parseFloat(low_price), close: parseFloat(close_price), volume: parseFloat(volume) }); } ``` ### Multiple Timeframes Subscribe to multiple intervals for the same symbol: ```javascript // Subscribe to multiple timeframes const subscriptions = [ { id: "sub-1m", method: "subscribe", params: { type: "candles", symbol: "BTC-USDT", timeframe: "1m" } }, { id: "sub-5m", method: "subscribe", params: { type: "candles", symbol: "BTC-USDT", timeframe: "5m" } }, { id: "sub-1h", method: "subscribe", params: { type: "candles", symbol: "BTC-USDT", timeframe: "1h" } } ]; subscriptions.forEach(sub => ws.send(JSON.stringify(sub))); ``` ### Use Cases #### Charting Applications * **Real-time Charts**: Update candlestick charts with live price data * **Technical Analysis**: Provide OHLCV data for technical indicators * **Multiple Timeframes**: Display different chart intervals simultaneously #### Trading Strategies * **Pattern Recognition**: Identify candlestick patterns for trading signals * **Trend Analysis**: Monitor price movements across different timeframes * **Volume Analysis**: Track volume patterns for market strength #### Data Collection * **Historical Data**: Build historical price databases * **Market Analysis**: Analyze market behavior and volatility * **Backtesting**: Collect data for strategy backtesting ### Implementation Notes * **Real-time Updates**: In-progress candles update continuously until closed * **Closed Candles**: Only add finalized candles to persistent storage * **Memory Management**: Limit the number of historical candles stored in memory * **Reconnection**: Re-subscribe on WebSocket reconnection to maintain data continuity ### Error Handling #### Subscription Errors ```json { "id": "sub-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid timeframe: 2m not supported" } } ``` #### Common Issues | Error | Description | Solution | | ------------------- | ----------------------------- | --------------------------------------------- | | Invalid timeframe | Unsupported timeframe | Use supported timeframes (1m, 5m, 1h, etc.) | | Invalid symbol | Symbol not available | Check supported trading pairs | | Subscription failed | Too many active subscriptions | Reduce the number of concurrent subscriptions | ### Related Endpoints * Get Candle History - Historical candle data via REST * [Get Candles](https://developers.synthetix.io/developer-resources/api/rest-api/info/getCandles) - Current candle data via REST * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time price updates * [Get Last Trades](https://developers.synthetix.io/developer-resources/api/rest-api/info/getLastTrades) - Individual trade executions via REST endpoint ## WebSocket Subscriptions Real-time data streams for market data and trading activity. ### Available Subscriptions #### Private Subscriptions (Authentication Required) | Subscription | Description | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | [SubAccount Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) | All subaccount activity: orders, trades, positions, margin updates, liquidations | #### Public Subscriptions (No Authentication) | Subscription | Description | | ---------------------------------------------------------------------------------------- | ----------------------------- | | [Candle Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/candleUpdates) | OHLCV candlestick data | | [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) | Real-time price ticker data | | [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) | L2 orderbook depth updates | | [Trade Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/tradeUpdates) | Real-time public trade stream | ### Connection **For public subscriptions** (Candle Updates, Market Price Updates, Orderbook Updates, Trade Updates): ``` wss://papi.synthetix.io/v1/ws/info ``` **For private subscriptions** (SubAccount Updates - requires authentication): ``` wss://papi.synthetix.io/v1/ws/trade ``` ### Subscribe Request Format ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "marketPrices", "symbol": "BTC-USDT" } } ``` ### Migration Guide import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; ## Market Price Updates Receive real-time price updates for individual price types (mark, last, index, mid) as they change. ### Overview The `marketPrices` subscription provides real-time updates for individual price types as they are published. Each price type (mark, last, index, mid) is sent as a separate update message when it changes. **Important**: Unlike the REST API which returns all price types in a single response, the WebSocket sends individual updates for each price type separately. ### Subscription Request #### Subscribe to Market Price Updates ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "marketPrices", "symbol": "BTC-USDT" } } ``` #### Subscribe to All Markets ```json { "id": "sub-all", "method": "subscribe", "params": { "type": "marketPrices", "symbol": "ALL" } } ``` Alternatively, omit the `symbol` parameter to subscribe to all markets (defaults to "ALL"): ```json { "id": "sub-all", "method": "subscribe", "params": { "type": "marketPrices" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"subscribe"` | | `params.type` | string | Yes | Must be `"marketPrices"` | | `params.symbol` | string | No | Trading pair symbol or `"ALL"` for all markets. Defaults to `"ALL"` if omitted | ### Price Update Messages #### Update Message Format ```json { "channel": "marketPriceUpdate", "data": { "symbol": "BTC-USDT", "price": "50125.50", "lastUpdateTime": "2025-01-03T12:34:56.789Z", "updateType": "mark" }, "timestamp": 1735907696789 } ``` #### Message Fields | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------- | | `symbol` | string | Trading pair symbol (e.g., "BTC-USDT") | | `price` | string | Price value for this update type | | `lastUpdateTime` | string | Timestamp when the price was published (RFC3339 format) | | `updateType` | string | Price type: `"mark"`, `"last"`, `"index"`, or `"mid"` | ### Price Types Each price type is sent as a separate update: | Type | Description | Example Use Case | | ------- | ------------------------------------------ | -------------------------------------- | | `mark` | Mark price used for margin calculations | Position valuation, liquidation checks | | `last` | Last traded price on the exchange | Current market price display | | `index` | Index price from external reference | Fair value reference | | `mid` | Mid-market price (average of best bid/ask) | Spread analysis | ### Update Examples #### Mark Price Update ```json { "channel": "marketPriceUpdate", "data": { "symbol": "BTC-USDT", "price": "50125.50", "lastUpdateTime": "2025-01-03T12:34:56.789Z", "updateType": "mark" }, "timestamp": 1735907696789 } ``` #### Index Price Update ```json { "channel": "marketPriceUpdate", "data": { "symbol": "ETH-USDT", "price": "3250.75", "lastUpdateTime": "2025-01-03T12:34:57.123Z", "updateType": "index" }, "timestamp": 1735907697123 } ``` #### Last Price Update ```json { "channel": "marketPriceUpdate", "data": { "symbol": "SOL-USDT", "price": "125.48", "lastUpdateTime": "2025-01-03T12:34:57.456Z", "updateType": "last" }, "timestamp": 1735907697456 } ``` #### Mid Price Update ```json { "channel": "marketPriceUpdate", "data": { "symbol": "BTC-USDT", "price": "50124.25", "lastUpdateTime": "2025-01-03T12:34:58.789Z", "updateType": "mid" }, "timestamp": 1735907698789 } ``` ### Implementation Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { ws.send(JSON.stringify({ id: 'sub-1', method: 'subscribe', params: { type: 'marketPrices', symbol: 'BTC-USDT' } })); }; // Cache prices by type const priceCache = new Map(); ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.channel === 'marketPriceUpdate') { const { symbol, price, updateType } = message.data; if (!priceCache.has(symbol)) { priceCache.set(symbol, {}); } priceCache.get(symbol)[updateType] = price; console.log(`${symbol} ${updateType}: ${price}`); } }; ``` ### Subscribe to All Markets ```javascript ws.send(JSON.stringify({ id: 'sub-all', method: 'subscribe', params: { type: 'marketPrices', symbol: 'ALL' } })); ``` ### Use Cases #### Trading Interfaces * **Price Tickers**: Display real-time mark, last, and index prices * **Price Type Comparison**: Compare different price types for arbitrage * **Latency Monitoring**: Track price update frequency and freshness #### Risk Management * **Mark Price Monitoring**: Track mark prices for margin calculations * **Price Divergence**: Monitor differences between price types * **Price Staleness**: Alert on outdated prices using `lastUpdateTime` #### Market Analysis * **Price Discovery**: Analyze how different price types move * **Spread Analysis**: Calculate spreads between mark and index * **Update Frequency**: Monitor how often each price type updates ### Implementation Notes * **Update Frequency**: Each price type updates independently when it changes * **Separate Messages**: You receive one message per price type, not a combined message * **Timestamp Format**: `lastUpdateTime` is in RFC3339 format * **Price Caching**: Maintain a local cache to have the latest price for each type * **Memory Management**: Clean up old price data to prevent memory leaks ### Comparison: WebSocket vs REST API | Feature | WebSocket `marketPrices` | REST `getMarketPrices` | | ------------ | ------------------------------ | --------------------------------------- | | Data Format | Individual price updates | Comprehensive market data | | Update Model | Push (as prices change) | Pull (on request) | | Fields | symbol, price, timestamp, type | All price types + funding + volume + OI | | Use Case | Real-time price tracking | Snapshot of all market data | | Message Size | Small (one price at a time) | Large (all data at once) | > **Note**: For comprehensive market data including funding rates, volume, and open interest, use the REST API endpoint [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices). ### Error Handling #### Subscription Errors ```json { "id": "sub-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid symbol: XYZ-USDT not supported" } } ``` #### Common Issues | Error | Description | Solution | | ------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------- | | Invalid symbol | Symbol not available | Check supported trading pairs via [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) | | Subscription failed | Too many active subscriptions | Reduce the number of concurrent subscriptions | | Connection timeout | WebSocket disconnected | Implement reconnection logic with exponential backoff | ### Related Endpoints * [Get Market Prices](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarketPrices) - Comprehensive market data via REST API (all price types, funding, volume, OI) * [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) - Available trading pairs * [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) - Real-time orderbook depth updates * [Candle Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/candleUpdates) - OHLCV candlestick data import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; ## Orderbook Updates Note: The subscription type is `"orderbook"` (lowercase). Some exchanges use `orderBook` (camelCase) for similar functionality. Receive live orderbook depth updates for specific trading pairs. Subscriptions support two delivery formats: **diff** (incremental changes, the default) and **snapshot** (full book on every update). ### Subscription Request Orderbook updates are **public data** and do not require authentication. #### Subscribe (Diff Mode — Default) ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT" } } ``` Equivalent to `format: "diff"`, `depth: 50`, `updateFrequencyMs: 250`. #### Subscribe (Snapshot Mode) ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT", "format": "snapshot" } } ``` #### Subscribe with Custom Parameters ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "orderbook", "symbol": "BTC-USDT", "format": "diff", "depth": 100, "updateFrequencyMs": 500 } } ``` #### Request Parameters | Parameter | Type | Required | Description | | -------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"subscribe"` | | `params.type` | string | Yes | Must be `"orderbook"` | | `params.symbol` | string | Yes | Trading pair symbol (e.g., `"BTC-USDT"`). Must be a specific symbol — `"ALL"` is not supported. | | `params.format` | string | No | `"diff"` (default) or `"snapshot"` | | `params.depth` | integer | No | Number of price levels per side: `10`, `50` (default), or `100` | | `params.updateFrequencyMs` | integer | No | Update interval in milliseconds (default: `250`). See [allowed values](#update-frequency) below. | #### Subscribe Response A successful subscribe response echoes the negotiated parameters: ```json { "id": "sub-1", "requestId": "sub-1", "status": 200, "result": { "type": "orderbook", "symbol": "BTC-USDT", "format": "diff", "depth": 50, "updateFrequencyMs": 250, "seq": 987654321 } } ``` The response echoes both `id` and `requestId` for backwards compatibility. The returned `seq` is the matching-engine sequence at subscription time. The first notification will have `meseq >= ` this value. ### Update Modes #### Diff Mode (Default) In diff mode, you receive: 1. An **initial snapshot** (`type: "snapshot"`) containing the full orderbook at your subscribed depth 2. **Incremental diffs** (`type: "diff"`) containing only the price levels that changed since the last update Diff mode significantly reduces bandwidth but requires you to maintain local orderbook state and apply updates. You must also validate continuity using the `meseq`/`prevMeseq` fields and verify integrity with the `checksum`. #### Snapshot Mode In snapshot mode, every message is a **full orderbook snapshot** at your subscribed depth. This is simpler to implement — just replace your local state with each message — but uses more bandwidth. ### Message Format #### Initial Snapshot (Diff Mode) When subscribing with `format: "diff"`, the first message is a full snapshot with `type: "snapshot"`. This establishes the baseline for subsequent diffs: ```json { "channel": "orderbookUpdate", "method": "orderbook_depth_update", "type": "snapshot", "meseq": 987654321, "met": 1704067200123456, "prevMeseq": null, "checksum": "a1b2c3d4", "data": { "symbol": "BTC-USDT", "timestamp": "2026-01-07T17:08:29Z", "bids": [ {"price": "100000.00", "quantity": "1.5"}, {"price": "99950.00", "quantity": "2.0"} ], "asks": [ {"price": "100050.00", "quantity": "1.2"}, {"price": "100100.00", "quantity": "1.8"} ] }, "timestamp": 1704067200000 } ``` > `method` is deprecated. Use `channel` to identify notification type. See [Message Fields](#message-fields). #### Diff Message Sent for subsequent updates in diff mode. Contains only the price levels that changed: ```json { "channel": "orderbookUpdate", "method": "orderbook_depth_update", "type": "diff", "meseq": 987654322, "met": 1704067201123456, "prevMeseq": 987654321, "checksum": "b2c3d4e5", "data": { "symbol": "BTC-USDT", "timestamp": "2026-01-07T17:08:30Z", "bids": [ {"price": "100000.00", "quantity": "1.2"}, {"price": "99900.00", "quantity": "0"} ], "asks": [ {"price": "100150.00", "quantity": "0.5"} ] }, "timestamp": 1704067200100 } ``` > `method` is deprecated. Use `channel` to identify notification type. See [Message Fields](#message-fields). In this example: * Bid at `100000.00` had its quantity updated from `1.5` to `1.2` * Bid at `99900.00` was removed (`quantity: "0"`) * A new ask level appeared at `100150.00` * All other levels remain unchanged #### Snapshot Mode Message When subscribing with `format: "snapshot"`, every message is a full orderbook. In snapshot mode the `type` field is omitted: ```json { "channel": "orderbookUpdate", "method": "orderbook_depth_update", "meseq": 987654321, "met": 1704067200123456, "checksum": "a1b2c3d4", "data": { "symbol": "BTC-USDT", "timestamp": "2026-01-07T17:08:29Z", "bids": [ {"price": "100000.00", "quantity": "1.5"}, {"price": "99950.00", "quantity": "2.0"} ], "asks": [ {"price": "100050.00", "quantity": "1.2"}, {"price": "100100.00", "quantity": "1.8"} ] }, "timestamp": 1704067200000 } ``` > `method` is deprecated. Use `channel` to identify notification type. See [Message Fields](#message-fields). #### Message Fields | Field | Type | Description | | ---------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `channel` | string | `"orderbookUpdate"` — the canonical channel identifier | | `method` | string | `"orderbook_depth_update"` (deprecated) — legacy identifier, included on all notifications for backwards compatibility. Use `channel` instead. | | `type` | string | `"snapshot"` for full book, `"diff"` for incremental changes. Omitted in snapshot-mode subscriptions. | | `meseq` | integer | Matching-engine sequence watermark for this update | | `met` | integer | Matching-engine event time in Unix microseconds | | `prevMeseq` | integer \| null | Expected previous matching-engine sequence. `null` on snapshots. | | `checksum` | string | CRC32 checksum of the full depth-truncated orderbook state after applying this message (see [Checksum Validation](#checksum-validation)) | | `data.symbol` | string | Trading pair symbol | | `data.timestamp` | string | RFC 3339 timestamp of the orderbook state (e.g., `"2026-01-07T17:08:29Z"`) | | `data.bids` | array | Bid price levels, sorted by price descending (highest first). An empty side is serialized as `[]`, never `null`. | | `data.asks` | array | Ask price levels, sorted by price ascending (lowest first). An empty side is serialized as `[]`, never `null`. | | `timestamp` | integer | Server send time in Unix milliseconds | ### Diff Semantics In a diff message, each price level in `bids` and `asks` represents a change: | Scenario | Representation | | ----------------- | ------------------------------------------------------------------------------------ | | **New level** | Level appears in the diff with a non-zero `quantity` | | **Updated level** | Level appears with the new `quantity` (replaces the previous quantity at that price) | | **Removed level** | Level appears with `quantity: "0"` | Levels **not** present in a diff message are unchanged. ### Price Level Format Each price level is an object with `price` and `quantity` fields: ```json {"price": "100000.50", "quantity": "1.25"} ``` Where: * `price` (string): Price level * `quantity` (string): Total quantity at this level. `"0"` in a diff means the level was removed. ### Depth The subscription `depth` parameter controls how many price levels per side are included. Available values: | Depth | Description | | ----- | -------------------------------------- | | `10` | Top 10 price levels per side | | `50` | Top 50 price levels per side (default) | | `100` | Top 100 price levels per side | For deeper snapshots, use the [Get Orderbook](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getOrderbook) request/response endpoint which supports depths up to 1000 levels. ### Update Frequency The `updateFrequencyMs` parameter controls the minimum interval between updates. The server batches changes within each interval and delivers a single message per tick. Allowed values: `50`, `100`, `250` (default), `500`, `1000`. **Constraint:** When `depth=100`, only `updateFrequencyMs` values of `250`, `500`, or `1000` are supported. ### Managing Local Orderbook #### Diff Mode When using diff mode, you must maintain local orderbook state and apply each diff as it arrives. ```javascript class OrderbookManager { constructor() { this.orderbooks = new Map(); // symbol → { bids: Map, asks: Map, meseq } } processMessage(message) { const { type, meseq, prevMeseq, checksum, data } = message; const { symbol } = data; if (type === "snapshot") { // Replace entire local state this.orderbooks.set(symbol, { bids: new Map(data.bids.map(l => [l.price, l.quantity])), asks: new Map(data.asks.map(l => [l.price, l.quantity])), meseq }); return; } // type === "diff" const book = this.orderbooks.get(symbol); if (!book) return; // No baseline — wait for snapshot // 1. Verify continuity using prevMeseq if (prevMeseq !== book.meseq) { // Gap or out-of-order — local state may be stale console.error(`Sequence gap for ${symbol}: expected prevMeseq=${book.meseq}, got ${prevMeseq}`); this.orderbooks.delete(symbol); this.resubscribe(symbol); return; } // 2. Apply changes for (const level of data.bids) { if (level.quantity === "0") { book.bids.delete(level.price); } else { book.bids.set(level.price, level.quantity); } } for (const level of data.asks) { if (level.quantity === "0") { book.asks.delete(level.price); } else { book.asks.set(level.price, level.quantity); } } book.meseq = meseq; // 3. Verify checksum if (!this.verifyChecksum(symbol, checksum)) { console.error(`Checksum mismatch for ${symbol} — resubscribing`); this.orderbooks.delete(symbol); this.resubscribe(symbol); } } getSortedBook(symbol) { const book = this.orderbooks.get(symbol); if (!book) return null; return { bids: [...book.bids.entries()] .sort((a, b) => parseFloat(b[0]) - parseFloat(a[0])) .map(([price, quantity]) => ({ price, quantity })), asks: [...book.asks.entries()] .sort((a, b) => parseFloat(a[0]) - parseFloat(b[0])) .map(([price, quantity]) => ({ price, quantity })) }; } } ``` #### Snapshot Mode In snapshot mode, each message is the complete orderbook — simply replace local state: ```javascript class SnapshotOrderbookManager { constructor() { this.orderbooks = new Map(); } processMessage(message) { const { data } = message; this.orderbooks.set(data.symbol, { bids: data.bids, asks: data.asks, timestamp: data.timestamp }); } } ``` #### Usage Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); const orderbookManager = new OrderbookManager(); ws.onopen = () => { ws.send(JSON.stringify({ id: "sub-1", method: "subscribe", params: { type: "orderbook", symbol: "BTC-USDT" } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.channel === "orderbookUpdate") { orderbookManager.processMessage(message); const book = orderbookManager.getSortedBook("BTC-USDT"); if (book && book.bids.length && book.asks.length) { console.log(`Best bid: ${book.bids[0].price} / Best ask: ${book.asks[0].price}`); } } }; ``` ### Checksum Validation The `checksum` field contains a CRC32 (IEEE) checksum of the full depth-truncated orderbook state **after** applying the message. Use it to verify that your local state matches the server's. #### Checksum Algorithm 1. Start with the depth-truncated orderbook after applying the update (sorted bids descending, asks ascending) 2. Build an input string by concatenating all levels: * For each bid: `b:|` * For each ask: `a:|` * Bids come first, then asks 3. Compute CRC32 (IEEE) of the resulting byte string 4. Format as an 8-character lowercase hexadecimal string **Example input string:** ``` b100000.00:1.5|b99950.00:2.0|a100050.00:1.2|a100100.00:1.8| ``` #### JavaScript Implementation ```javascript import crc32 from 'crc-32'; // npm install crc-32 function computeChecksum(bids, asks) { let input = ''; for (const [price, quantity] of bids) { input += `b${price}:${quantity}|`; } for (const [price, quantity] of asks) { input += `a${price}:${quantity}|`; } const value = crc32.buf(Buffer.from(input)) >>> 0; // unsigned return value.toString(16).padStart(8, '0'); } ``` **Important:** The checksum is computed over the **depth-truncated** book at your subscribed depth. If your local book has accumulated more levels than your subscribed depth (e.g., from levels moving in and out of range), truncate to the top N levels per side before computing. ### Recovery #### Diff Mode Recovery If any of the following occur, discard your local orderbook and resubscribe: * **Continuity break:** A diff message's `prevMeseq` does not match your last stored `meseq`, indicating a gap or out-of-order delivery * **Checksum mismatch:** Your locally computed checksum does not match the message `checksum` * **Missing baseline:** You receive a diff but have no local snapshot Resubscribing gives you a fresh snapshot to re-establish your baseline: ```javascript function resubscribe(ws, symbol) { ws.send(JSON.stringify({ id: "resub-" + Date.now(), method: "subscribe", params: { type: "orderbook", symbol } })); } ``` #### Fetching a Snapshot Manually You can also fetch a one-time snapshot via request/response to sync your state without resubscribing: * [Get Orderbook (REST)](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) * [Get Orderbook (WebSocket)](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getOrderbook) #### Snapshot Mode Recovery In snapshot mode, reconnection is simple — each message is a full book, so just reconnect and resubscribe. ### Performance Optimization #### Backpressure: Slow Consumer Disconnection If your client cannot consume messages fast enough, the server will **silently remove your subscription**. The WebSocket send buffer has a fixed capacity, and when it fills up, the server drops the subscriber rather than blocking other clients. You will not receive an error message — the orderbook updates will simply stop arriving. To avoid this: * Process incoming messages promptly (avoid blocking the WebSocket message handler) * Choose an appropriate `updateFrequencyMs` for your use case — slower frequencies produce fewer messages * Monitor for gaps in updates and resubscribe if needed #### Choosing Parameters | Use Case | Recommended Settings | | ---------------------- | ----------------------------------------------------------- | | HFT / market making | `depth: 10`, `updateFrequencyMs: 50`, `format: "diff"` | | Trading UI | `depth: 50`, `updateFrequencyMs: 250`, `format: "diff"` | | Analytics / monitoring | `depth: 50`, `updateFrequencyMs: 500`, `format: "snapshot"` | | Simple integration | `depth: 10`, `format: "snapshot"` | ### Authentication Orderbook updates are **public data** and do not require authentication. They are available on the public `/info` endpoint. ### Common Issues | Issue | Description | Solution | | ----------------- | ------------------------------------------- | ----------------------------------------------------------------------------- | | Connection lost | WebSocket disconnection | Reconnect and resubscribe | | Updates stopped | Slow consumer — server removed subscription | Choose a slower `updateFrequencyMs`, process messages faster, and resubscribe | | Checksum mismatch | Local state has diverged from server | Discard local state and resubscribe | | Invalid symbol | Symbol not available | Check supported markets | | `"ALL"` rejected | Wildcard subscriptions not supported | Subscribe to each symbol individually | ### Implementation Notes * **Default is diff mode**: Subscriptions default to `format: "diff"` — you must maintain local state and apply updates * **Configurable depth**: Choose 10, 50, or 100 levels per side * **Timer-driven delivery**: Updates are batched and sent at the configured `updateFrequencyMs` interval * **Checksum on every message**: Both snapshot and diff messages include a CRC32 checksum for validation * **No `"ALL"` symbol**: You must subscribe to each symbol individually * **Snapshot fallback**: In diff mode, if changes are very large the server may send a snapshot instead of a diff — always check the `type` field ### Related Endpoints * [Get Orderbook](https://developers.synthetix.io/developer-resources/api/rest-api/info/getOrderbook) - Get orderbook snapshot via REST * [Get Orderbook (WebSocket)](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getOrderbook) - Get orderbook snapshot with configurable depth via WebSocket request/response * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Track price changes * [Trade Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/tradeUpdates) - Real-time public trade stream import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; ## SubAccount Updates Consolidated real-time stream of all subaccount activity including orders, trades, margin updates, and liquidations. This subscription replaces the need to subscribe to multiple separate streams. ### Subscription Request #### Subscribe to SubAccount Updates ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "subAccountUpdates", "subAccountId": "1867542890123456789" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"subscribe"` | | `params.type` | string | Yes | Must be `"subAccountUpdates"` | | `params.subAccountId` | string | Yes | Subaccount ID to receive events for | ### Event Types | Event Type | Description | | ---------------------------------- | ---------------------------------------------- | | `orderPlaced` | Order successfully placed on orderbook | | `orderFilled` | Order fully executed | | `orderPartiallyFilled` | Order partially executed | | `orderCancelled` | Order cancelled by user or system | | `orderModified` | Order price or quantity modified | | `orderRejected` | Order rejected due to validation failure | | `trade` | Trade execution with fill details | | `marginUpdate` | Account margin/balance state updated | | `liquidation` | Position liquidated due to insufficient margin | | `delegationAdded` | Delegation created or refreshed for a signer | | `delegationRevoked` | Delegation removed for a signer | | `funding` | Funding payment for open positions | | `wickInsurancePositionIncreased` | Wick insurance position size increased | | `wickInsuranceProtectionActivated` | Wick insurance protection activated | | `wickInsuranceProtectionCompleted` | Wick insurance protection completed | Order events can include additive optional `expiresAt` and `cancelReason` fields. Trade events include additive `direction`, `order` (with `venueId`/`clientId`), `maker`, `reduceOnly`, `postOnly`, `markPrice`, `entryPrice`, and `triggeredByLiquidation` fields, and the `position` object now includes `netFunding`. `marginUpdate` notifications include an additive `debt` field. `funding` notifications include additive `paymentTime`, `fundingTime` (replacing the deprecated `timestamp` and `fundingTimestamp`). If your client uses an exhaustive `switch` over `eventType`, keep a safe default branch so future additive event types do not break message handling. ### User Event Messages #### Order Placed ```json { "channel": "subAccountUpdate", "data": { "eventType": "orderPlaced", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360", "symbol": "BTC-USDT", "timestamp": 1704067200000, "side": "buy", "orderType": "limit", "price": "50000.00", "quantity": "0.1", "filledQuantity": "0", "remainingQuantity": "0.1", "direction": "long", "status": "OrderStatePlaced", "createdAt": 1704067199998, "placedAt": 1704067200000, "updatedAt": 1704067200000, "clientOrderId": "0x1234567890abcdef1234567890abcdef" }, "timestamp": 1704067200000 } ``` #### Order Filled ```json { "channel": "subAccountUpdate", "data": { "eventType": "orderFilled", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360", "symbol": "BTC-USDT", "timestamp": 1704067210000, "side": "buy", "orderType": "limit", "price": "50000.00", "quantity": "0.1", "filledQuantity": "0.1", "remainingQuantity": "0", "direction": "long", "status": "OrderStateFilled", "createdAt": 1704067199998, "placedAt": 1704067200000, "updatedAt": 1704067210000, "clientOrderId": "0x1234567890abcdef1234567890abcdef" }, "timestamp": 1704067210000 } ``` #### Order Cancelled ```json { "channel": "subAccountUpdate", "data": { "eventType": "orderCancelled", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360", "symbol": "BTC-USDT", "timestamp": 1704067220000, "side": "buy", "orderType": "limit", "price": "50000.00", "quantity": "0.1", "filledQuantity": "0", "remainingQuantity": "0.1", "direction": "long", "status": "OrderStateCancelled", "cancelledAt": 1704067220000, "createdAt": 1704067199998, "placedAt": 1704067200000, "updatedAt": 1704067220000, "clientOrderId": "0x1234567890abcdef1234567890abcdef" }, "timestamp": 1704067220000 } ``` #### Order Rejected ```json { "channel": "subAccountUpdate", "data": { "eventType": "orderRejected", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360", "symbol": "BTC-USDT", "timestamp": 1704067200000, "side": "buy", "orderType": "limit", "price": "50000.00", "quantity": "0.1", "direction": "long", "status": "OrderStateRejected", "reason": "Insufficient margin", "createdAt": 1704067199998, "updatedAt": 1704067200000, "clientOrderId": "0x1234567890abcdef1234567890abcdef" }, "timestamp": 1704067200000 } ``` #### Trade Execution ```json { "channel": "subAccountUpdate", "data": { "eventType": "trade", "tradeId": "123456790", "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "orderId": "1948058938469519360", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "price": "50010.00", "quantity": "0.1", "fee": "5.00", "feeRate": "0.001", "realizedPnl": "0.00", "isTaker": true, "maker": false, "reduceOnly": false, "postOnly": false, "markPrice": "50025.00", "entryPrice": "50010.00", "timestamp": 1704067230000, "tradedAt": 1704067230000, "position": { "adlBucket": 2, "side": "long", "size": "0.1", "entryPrice": "50010.00", "unrealizedPnl": "0.00", "netFunding": "0.00" } }, "timestamp": 1704067230000 } ``` ##### Trade event fields | Field | Type | Description | | --------------------- | ------- | ------------------------------------------------------------------------------- | | `tradeId` | string | Unique trade execution identifier | | `order` | object | Canonical composite order identifier (`venueId`, optional `clientId`) | | `order.venueId` | string | Venue-generated order identifier | | `order.clientId` | string | Optional client-provided order identifier | | `orderId` | string | Deprecated legacy venue identifier | | `subAccountId` | string | Subaccount identifier | | `symbol` | string | Trading pair symbol | | `side` | string | Trade side: `"buy"` or `"sell"` | | `direction` | string | Position effect: `"open long"`, `"close long"`, `"open short"`, `"close short"` | | `price` | string | Filled price | | `quantity` | string | Filled quantity | | `fee` | string | Trading fee amount | | `feeRate` | string | Fee rate applied | | `realizedPnl` | string | Realized P\&L from position closure | | `isTaker` | boolean | Deprecated; prefer `maker` | | `maker` | boolean | `true` if the trade was maker (provided liquidity), `false` if taker | | `reduceOnly` | boolean | `true` if the trade reduced an existing position | | `postOnly` | boolean | `true` if the order was post-only (maker-only) | | `markPrice` | string | Mark price at time of trade | | `entryPrice` | string | Position average entry price at time of trade | | `timestamp` | integer | Trade timestamp (Unix milliseconds) | | `tradedAt` | integer | Trade execution timestamp (Unix milliseconds) | | `position` | object | Post-trade position snapshot | | `position.netFunding` | string | Net funding accrued on the position (positive = received) | #### Margin Update ```json { "channel": "subAccountUpdate", "data": { "eventType": "marginUpdate", "subAccountId": "1867542890123456789", "accountValue": "10000.00", "availableMargin": "8500.00", "totalUnrealizedPnl": "125.50", "maintenanceMargin": "750.00", "initialMargin": "1500.00", "withdrawable": "8500.00", "adjustedAccountValue": "9875.50", "debt": "0.00", "timestamp": 1704067800000, "position": { "adlBucket": 3, "symbol": "BTC-USDT", "upnl": "125.50", "initialMargin": "750.00", "maintenanceMargin": "375.00", "markPrice": "50125.50" } }, "timestamp": 1704067800000 } ``` ##### Margin update fields | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------------------------------------- | | `subAccountId` | string | Subaccount identifier | | `accountValue` | string | Total account value (equity) | | `availableMargin` | string | Margin available for opening new positions | | `totalUnrealizedPnl` | string | Aggregated unrealized P\&L across all positions | | `maintenanceMargin` | string | Total maintenance margin requirement | | `initialMargin` | string | Total initial margin requirement | | `withdrawable` | string | Amount currently withdrawable | | `adjustedAccountValue` | string | Account value adjusted for unrealized P\&L | | `debt` | string | Outstanding debt on the subaccount (e.g. unpaid funding). `"0.00"` when no debt is owed. | | `timestamp` | integer | Event timestamp (Unix milliseconds) | | `position` | object | Optional per-position snapshot included when the update is triggered by a specific position | #### Liquidation ```json { "channel": "subAccountUpdate", "data": { "eventType": "liquidation", "tradeId": "123456791", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "sell", "price": "40000.00", "quantity": "0.2", "realizedPnl": "-2000.00", "fee": "20.00", "feeRate": "0.005", "liquidationPrice": "40000.00", "timestamp": 1704067900000, "position": { "adlBucket": 0, "side": null, "size": "0.0", "entryPrice": "0.0", "unrealizedPnl": "0.00" } }, "timestamp": 1704067900000 } ``` #### Funding ```json { "channel": "subAccountUpdate", "data": { "eventType": "funding", "paymentId": "fp_1958787130134106112", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "positionSize": "0.2", "fundingRate": "0.0000125", "payment": "-3.625312", "markPrice": "50000.00", "timestamp": 1704067800000, "paymentTime": 1704067800000, "fundingTimestamp": 1704067800000, "fundingTime": 1704067800000 }, "timestamp": 1704067800000 } ``` ##### Funding event fields | Field | Type | Description | | ------------------ | ------- | -------------------------------------------------------------------------------------- | | `paymentId` | string | Unique identifier for the funding payment | | `subAccountId` | string | Subaccount identifier | | `symbol` | string | Trading pair symbol | | `positionSize` | string | Position size at funding time (signed) | | `fundingRate` | string | Funding rate applied | | `payment` | string | Funding payment amount (negative = paid out) | | `markPrice` | string | Mark price used for payment calculation | | `timestamp` | integer | **DEPRECATED** - use `paymentTime`. When the payment was processed (Unix milliseconds) | | `paymentTime` | integer | When the payment was processed (Unix milliseconds) — replaces `timestamp` | | `fundingTimestamp` | integer | **DEPRECATED** - use `fundingTime`. Funding period timestamp (Unix milliseconds) | | `fundingTime` | integer | Funding period timestamp (Unix milliseconds) — replaces `fundingTimestamp` | #### Delegation Added ```json { "channel": "subAccountUpdate", "data": { "eventType": "delegationAdded", "subAccountId": "1867542890123456789", "delegateAddress": "0x1234567890abcdef1234567890abcdef12345678", "permissions": ["trade", "transfer"], "expiresAt": 1767225600000, "addedBy": "0xabcdef1234567890abcdef1234567890abcdef12", "timestamp": 1704068000000 }, "timestamp": 1704068000000 } ``` #### Delegation Revoked ```json { "channel": "subAccountUpdate", "data": { "eventType": "delegationRevoked", "subAccountId": "1867542890123456789", "delegateAddress": "0x1234567890abcdef1234567890abcdef12345678", "timestamp": 1704068100000 }, "timestamp": 1704068100000 } ``` #### Wick Insurance - Position Increased ```json { "channel": "subAccountUpdate", "data": { "eventType": "wickInsurancePositionIncreased", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "oldQuantity": "0.1", "newQuantity": "0.2", "timestamp": 1704067200000 }, "timestamp": 1704067200000 } ``` #### Wick Insurance - Protection Activated ```json { "channel": "subAccountUpdate", "data": { "eventType": "wickInsuranceProtectionActivated", "subAccountId": "1867542890123456789", "expiredTime": 1704067800000, "timestamp": 1704067200000 }, "timestamp": 1704067200000 } ``` #### Wick Insurance - Protection Completed ```json { "channel": "subAccountUpdate", "data": { "eventType": "wickInsuranceProtectionCompleted", "subAccountId": "1867542890123456789", "protectionId": "12345", "expiredTime": 1704067800000, "timestamp": 1704067200000 }, "timestamp": 1704067200000 } ``` ### Event Data Fields #### Common Fields | Field | Type | Description | | -------------- | ------- | ------------------------------- | | `eventType` | string | Type of event | | `subAccountId` | string | Affected subaccount | | `timestamp` | integer | Event timestamp in milliseconds | | `symbol` | string | Trading pair (when applicable) | #### Order Fields | Field | Type | Description | | ------------------- | ------- | ----------------------------------------------------------------------------- | | `orderId` | string | Unique order identifier | | `clientOrderId` | string | Client-provided order ID | | `side` | string | Order side: `"buy"` or `"sell"` | | `orderType` | string | Order type: `"limit"`, `"market"`, `"stopLoss"`, `"takeProfit"` | | `price` | string | Order price (empty for market orders) | | `quantity` | string | Total order quantity | | `filledQuantity` | string | Amount already filled | | `remainingQuantity` | string | Amount remaining to fill | | `direction` | string | Position direction: `"long"` or `"short"` | | `status` | string | Order status (see Order Status Values below) | | `reason` | string | Cancellation/rejection reason | | `createdAt` | integer | Order creation timestamp in milliseconds | | `placedAt` | integer | Order placement timestamp in milliseconds | | `cancelledAt` | integer | Order cancellation timestamp in milliseconds (cancelled orders only) | | `updatedAt` | integer | Order last update timestamp in milliseconds | | `expiresAt` | integer | Order expiry timestamp in milliseconds, when the order has an explicit expiry | | `cancelReason` | string | Machine-readable cancellation reason when supplied by the backend | #### Order Status Values | Status | Description | | --------------------------- | ------------------------------ | | `OrderStatePlaced` | Order placed on orderbook | | `OrderStatePartiallyFilled` | Order partially executed | | `OrderStateFilled` | Order fully executed | | `OrderStateCancelled` | Order cancelled | | `OrderStateRejected` | Order rejected | | `OrderStateModify` | Order modification in progress | | `OrderStateModified` | Order successfully modified | #### Trade Fields | Field | Type | Description | | ------------------------ | ------- | ------------------------------------------------------------ | | `tradeId` | string | Unique trade identifier | | `orderId` | string | Order ID that generated this trade | | `fee` | string | Trading fee amount | | `feeRate` | string | Fee rate applied | | `realizedPnl` | string | Realized P\&L from trade | | `isTaker` | boolean | Whether this was a taker trade | | `timestamp` | integer | Event timestamp in milliseconds (deprecated, use `tradedAt`) | | `tradedAt` | integer | Trade execution timestamp in milliseconds | | `liquidationPrice` | string | Liquidation price (liquidations only) | | `direction` | string | Position direction associated with the fill, when included | | `markPrice` | string | Mark price at execution time, when included | | `entryPrice` | string | Average entry price relevant to the execution, when included | | `maker` | boolean | Whether the fill executed as maker liquidity | | `reduceOnly` | boolean | Whether the order was reduce-only | | `postOnly` | boolean | Whether the order was post-only | | `triggeredByLiquidation` | boolean | Whether the trade was triggered by liquidation flow | #### Position Snapshot (in Trade Events) | Field | Type | Description | | ------------------------ | -------------- | ---------------------------------------------------------------------------------------- | | `position.adlBucket` | integer | ADL priority bucket (1–5). 5 = highest risk, 1 = lowest risk. 0 when position is closed. | | `position.side` | string \| null | Position side after event | | `position.size` | string | Position size after event | | `position.entryPrice` | string | Average entry price | | `position.unrealizedPnl` | string | Current unrealized P\&L | | `position.netFunding` | string | Cumulative net funding received/paid for this position | #### Margin Update Fields | Field | Type | Description | | ---------------------------- | ------- | ------------------------------------------------------------- | | `accountValue` | string | Total account value including unrealized P\&L | | `availableMargin` | string | Margin available for new positions | | `totalUnrealizedPnl` | string | Sum of unrealized P\&L across all positions | | `maintenanceMargin` | string | Required margin to maintain positions | | `initialMargin` | string | Required margin to open positions | | `withdrawable` | string | Amount available for withdrawal | | `adjustedAccountValue` | string | Account value adjusted for risk | | `position` | object | Optional position that triggered update | | `position.adlBucket` | integer | ADL priority bucket (1–5). 5 = highest risk, 1 = lowest risk. | | `position.symbol` | string | Market symbol | | `position.upnl` | string | Position unrealized P\&L | | `position.initialMargin` | string | Position initial margin requirement | | `position.maintenanceMargin` | string | Position maintenance margin requirement | | `position.markPrice` | string | Current mark price | #### Delegation Fields | Field | Type | Description | | ----------------- | ------- | --------------------------------------------------------------------------------------------- | | `delegateAddress` | string | Wallet address whose delegation was added or revoked | | `permissions` | array | Delegated permissions granted to the signer (`delegationAdded` only) | | `expiresAt` | integer | Expiry timestamp in milliseconds when the delegation is time-bounded (`delegationAdded` only) | | `addedBy` | string | Wallet address that added or refreshed the delegation (`delegationAdded` only) | | `timestamp` | integer | Event timestamp in milliseconds | #### Funding Fields | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------------- | | `paymentId` | string | Unique funding payment identifier | | `symbol` | string | Trading pair | | `positionSize` | string | Position size at time of funding | | `fundingRate` | string | Funding rate applied | | `payment` | string | Funding payment amount (negative = paid, positive = received) | | `markPrice` | string | Mark price at time of funding | #### Wick Insurance Fields | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------------- | | `symbol` | string | Trading pair (position increased only) | | `oldQuantity` | string | Previous position size (position increased only) | | `newQuantity` | string | New position size (position increased only) | | `expiredTime` | integer | Protection expiry time in milliseconds (protection events) | | `protectionId` | string | Protection identifier (protection completed only) | ### Implementation Example ```javascript // Subscribe to all subaccount updates const subscription = { id: "sub-1", method: "subscribe", params: { type: "subAccountUpdates", subAccountId: "1867542890123456789" } }; ws.send(JSON.stringify(subscription)); // Handle subaccount events ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.channel === "subAccountUpdate") { const { eventType, symbol } = message.data; switch (eventType) { case "orderPlaced": console.log(`Order placed: ${message.data.orderId}`); updateOrdersUI(message.data); break; case "orderFilled": case "orderPartiallyFilled": console.log(`Order ${eventType}: ${message.data.orderId}`); updateOrdersUI(message.data); break; case "orderCancelled": console.log(`Order cancelled: ${message.data.orderId}`); updateOrdersUI(message.data); break; case "trade": console.log(`Trade executed: ${message.data.tradeId}`); updateTradesUI(message.data); updatePositionsUI(message.data.position); break; case "marginUpdate": console.log(`Margin updated for account: ${message.data.subAccountId}`); updateMarginUI(message.data); if (message.data.position) { updatePositionMarginUI(message.data.position); } break; case "liquidation": console.log(`LIQUIDATION: ${symbol} at ${message.data.price}`); handleLiquidation(message.data); break; case "delegationAdded": case "delegationRevoked": console.log(`Delegation update: ${eventType}`); handleDelegationUpdate(message.data); break; default: console.log(`${eventType} event:`, message.data); } } }; ``` ### Benefits #### Single Subscription * One WebSocket connection for all user activity * Simplified client implementation * Reduced connection overhead #### Complete Context * Each event includes all relevant data * Position snapshots after trades * No need to correlate multiple streams #### Consistent Format * Unified event structure * Standard field naming * Predictable data types ### Migration from Separate Subscriptions If you're currently using separate subscriptions: 1. **Replace multiple subscriptions** with single `subAccountUpdates` subscription 2. **Update event handlers** to use `eventType` field and `"subAccountUpdate"` channel 3. **Remove correlation logic** - each event is self-contained 4. **Simplify state management** - position data included in trade events 5. **Add margin update handling** - receive real-time margin/balance updates #### Before (Multiple Subscriptions) ```javascript // Old approach - multiple subscriptions subscribeToOrders(subAccountId, handleOrderUpdate); subscribeToTrades(subAccountId, handleTradeUpdate); ``` #### After (Single Subscription) ```javascript // New approach - single subscription subscribeToSubAccountUpdates(subAccountId, (event) => { switch(event.eventType) { case "orderPlaced": case "orderFilled": case "orderPartiallyFilled": case "orderCancelled": case "orderModified": case "orderRejected": handleOrderUpdate(event); break; case "trade": handleTradeUpdate(event); break; case "marginUpdate": handleMarginUpdate(event); break; case "liquidation": handleLiquidation(event); break; case "delegationAdded": case "delegationRevoked": handleDelegationUpdate(event); break; default: handleUnknownSubAccountEvent(event); } }); ``` ### Error Handling #### Subscription Errors ```json { "id": "sub-1", "status": 401, "result": null, "error": { "code": 401, "message": "Invalid subaccount ID" } } ``` #### Common Issues | Error | Description | Solution | | ----------------------- | ------------------------------------- | ------------------------------- | | Invalid subaccount ID | Subaccount doesn't exist or no access | Verify subaccount ownership | | Authentication required | WebSocket not authenticated | Complete authentication first | | Rate limit exceeded | Too many events | Implement backpressure handling | ### Related Endpoints * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Required before subscribing to this stream * [Get Positions](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Current position state via REST API * [Get Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOrdersHistory) - Historical order data via REST API * [Get Trades](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - Historical trade data via REST API * [Get SubAccount](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccount) - Account information via REST API import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; ## Trade Updates Receive real-time public trade notifications as they occur on the exchange. ### Overview The `trades` subscription provides a real-time stream of trades as they are executed. Each message contains details about a single trade including price, quantity, side, and timing information. ### Subscription Request #### Subscribe to a Specific Symbol ```json { "id": "sub-1", "method": "subscribe", "params": { "type": "trades", "symbol": "BTC-USDT" } } ``` #### Subscribe to All Symbols ```json { "id": "sub-all", "method": "subscribe", "params": { "type": "trades", "symbol": "ALL" } } ``` Alternatively, omit the `symbol` parameter to subscribe to all symbols (defaults to "ALL"): ```json { "id": "sub-all", "method": "subscribe", "params": { "type": "trades" } } ``` #### Request Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"subscribe"` | | `params.type` | string | Yes | Must be `"trades"` | | `params.symbol` | string | No | Trading pair symbol (e.g., "BTC-USDT") or `"ALL"` for all symbols. Defaults to `"ALL"` if omitted | ### Unsubscription Request ```json { "id": "unsub-1", "method": "unsubscribe", "params": { "type": "trades", "symbol": "BTC-USDT" } } ``` The `type` and `symbol` must match the original subscription request. ### Trade Notification Messages #### Message Format ```json { "channel": "trade", "data": { "tradeId": "12345", "symbol": "BTC-USDT", "side": "buy", "price": "50125.50", "quantity": "0.15", "timestamp": "2025-01-03T12:34:56.789Z", "isMaker": false }, "timestamp": 1735907696789 } ``` #### Message Fields | Field | Type | Description | | ----------- | ------- | -------------------------------------------- | | `tradeId` | string | Unique trade identifier | | `symbol` | string | Trading pair symbol (e.g., "BTC-USDT") | | `side` | string | Taker side of the trade: `"buy"` or `"sell"` | | `price` | string | Execution price | | `quantity` | string | Trade quantity | | `timestamp` | string | Time of trade execution (RFC3339 format) | | `isMaker` | boolean | Whether the reported side was the maker | ### Implementation Example ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/info'); ws.onopen = () => { ws.send(JSON.stringify({ id: 'sub-1', method: 'subscribe', params: { type: 'trades', symbol: 'BTC-USDT' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.channel === 'trade') { const { tradeId, symbol, side, price, quantity, timestamp } = message.data; console.log(`[${timestamp}] ${symbol} ${side} ${quantity} @ ${price} (trade ${tradeId})`); } }; ``` ### Use Cases #### Trading Interfaces * **Trade Feed**: Display a live trade ticker showing recent executions * **Time & Sales**: Build a time-and-sales panel with trade-by-trade data * **Trade Alerts**: Trigger notifications for large trades #### Market Analysis * **Volume Tracking**: Aggregate trade quantities over time windows * **Buy/Sell Pressure**: Monitor the ratio of buy vs sell trades * **Price Impact**: Observe how individual trades move the market #### Data Feeds * **Trade History**: Build a local trade history database * **VWAP Calculation**: Compute volume-weighted average price in real time * **Tick Data**: Capture every trade for backtesting or analytics ### Implementation Notes * **Public Data**: No authentication required — connects to the info WebSocket endpoint * **Side**: The `side` field represents the taker side of the trade * **Default Symbol**: Omitting `symbol` defaults to `"ALL"`, streaming trades for every market * **Message Frequency**: High-volume markets may produce a large number of messages — consider filtering by symbol if you only need specific pairs ### Error Handling #### Subscription Errors ```json { "id": "sub-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid symbol: XYZ-USDT not supported" } } ``` #### Common Issues | Error | Description | Solution | | ------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------- | | Invalid symbol | Symbol not available | Check supported trading pairs via [Get Markets](https://developers.synthetix.io/developer-resources/api/rest-api/info/getMarkets) | | Subscription failed | Too many active subscriptions | Reduce the number of concurrent subscriptions | | Connection timeout | WebSocket disconnected | Implement reconnection logic with exponential backoff | ### Related Endpoints * [Get Last Trades](https://developers.synthetix.io/developer-resources/api/rest-api/info/getLastTrades) - Recent trades via REST API * [Get Last Trades (WebSocket)](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getLastTrades) - Recent trades via WebSocket query * [Market Price Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/marketPriceUpdates) - Real-time price ticker data * [Orderbook Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/orderbookUpdates) - Real-time orderbook depth updates import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Add Delegated Signer (WebSocket) Add a delegated signer to a subaccount through the WebSocket connection, allowing another wallet address to perform authorized actions on behalf of the subaccount. This enables secure delegation of trading operations to automated systems, trading bots, or team members without sharing private keys. ### Request #### Request Format ```json { "id": "delegate-add-1", "method": "post", "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | --------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"addDelegatedSigner"` | | `subAccountId` | string | Yes | Subaccount identifier | | `walletAddress` | string | Yes | Ethereum wallet address of the delegated signer (42-character hex format). Note: this request field is `walletAddress`; the EIP-712 signed message uses `delegateAddress` for the same value | | `permissions` | string\[] | Yes | Single-item permission array. Use `["session"]` for trading access or `["delegate"]` for trading access plus the ability to create session-level delegations. Older clients may still submit `["trading"]`, which is treated as `["session"]`. | | `expiresAt` | integer | No | Optional Unix timestamp (milliseconds) when the delegation expires | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional request expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature | **Important**: Owners can add `session` or `delegate` signers. `delegate` signers can add `session` signers only. #### EIP-712 Type Definition ### Response Format #### Success Response ```json { "id": "delegate-add-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "expiresAt": null } } ``` #### Response Fields | Field | Type | Description | | --------------- | --------------- | --------------------------------------------------- | | `subAccountId` | string | The subaccount ID the signer was added to | | `walletAddress` | string | The delegated signer's wallet address | | `permissions` | string\[] | Single-item permission array returned by the server | | `expiresAt` | integer \| null | Expiration timestamp (null if no expiration) | #### Error Response ```json { "id": "delegate-add-1", "status": 400, "result": null, "error": { "code": 400, "message": "Delegated signer already exists" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function addDelegatedSigner(ws, signer, subAccountId, delegateAddress, permissions, expiresAt = 0) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { AddDelegatedSigner: [ { name: "delegateAddress", type: "address" }, { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" }, { name: "expiresAt", type: "uint256" }, { name: "permissions", type: "string[]" } ] }; const message = { subAccountId: BigInt(subAccountId), delegateAddress, permissions, expiresAt: BigInt(expiresAt), nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); ws.send(JSON.stringify({ id: `delegate-add-${Date.now()}`, method: "post", params: { action: "addDelegatedSigner", subAccountId, walletAddress: delegateAddress, permissions, expiresAt: expiresAt || undefined, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Add a session-level trading bot as delegated signer await addDelegatedSigner( ws, signer, "1867542890123456789", "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", ["session"] ); ``` ### Code Examples #### Add Session-Level Trading Bot Signer ```json { "id": "delegate-add-bot", "method": "post", "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Add Temporary Delegated Signer with Expiration ```json { "id": "delegate-add-temp", "method": "post", "params": { "action": "addDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "permissions": ["session"], "expiresAt": 1767225600000, "nonce": 1735689600001, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Delegate Object Structure ### Implementation Notes * **Caller Permissions**: Owners can add `session` or `delegate` signers. `delegate` signers can add `session` signers only * **Multiple Signers**: A subaccount can have multiple delegated signers simultaneously * **Unique Addresses**: Each wallet address can only be delegated once per subaccount * **Permission Levels**: Use `["session"]` for trading access or `["delegate"]` for trading access plus the ability to create session-level delegations * **Immediate Effect**: Delegations take effect immediately upon successful creation * **Optional Expiration**: Delegations can include an expiration timestamp after which they become invalid * **Revocable**: Delegations can be revoked at any time using the `removeDelegatedSigner` endpoint ### Common Errors | Error Code | Message | Description | | ---------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | 400 | Delegated signer already exists | Address is already delegated for this subaccount | | 400 | Maximum delegated signers limit reached | Too many delegates on this subaccount | | 400 | Cannot delegate to self | Accounts cannot delegate to themselves | | 403 | Caller is not authorized to add the requested delegation | Session signers cannot create delegations, and delegate signers cannot grant delegate-level access | | 404 | Subaccount not found | Invalid subaccount ID | ### Next Steps * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/removeDelegatedSigner) - Revoke delegation * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - List all delegated signers * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Connection setup * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/addDelegatedSigner) - REST API comparison ## WebSocket Authentication The Trade WebSocket requires authentication using EIP-712 signatures before any trading operations can be performed. This document covers WebSocket-specific implementation details. ### Authentication Flow 1. **Connect** to the WebSocket endpoint 2. **Send** authentication message within 30 seconds 3. **Receive** authentication confirmation 4. **Begin** trading operations ### Authentication Message Send this message immediately after connection: ```json { "id": "auth-1", "method": "auth", "params": { "message": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"AuthMessage\":[{\"name\":\"subAccountId\",\"type\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\"},{\"name\":\"action\",\"type\":\"string\"}]},\"primaryType\":\"AuthMessage\",\"domain\":{\"name\":\"Synthetix\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0x0000000000000000000000000000000000000000\"},\"message\":{\"subAccountId\":\"0x19f2e3c8b5a7d1f0\",\"timestamp\":\"0x187a3e4f2b1c\",\"action\":\"websocket_auth\"}}", "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12" } } ``` #### Parameters | Parameter | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"auth"` | | `params.message` | string | Yes | JSON-stringified EIP-712 structured data (types, domain, primaryType, message) | | `params.signature` | string | Yes | Raw EIP-712 signature as hex string (65 bytes: 0x + 130 hex chars) | ### EIP-712 Signature The authentication signature uses this structure: #### Domain ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; ``` #### Types ```javascript const types = { AuthMessage: [ { name: "subAccountId", type: "uint256" }, { name: "timestamp", type: "uint256" }, { name: "action", type: "string" } ] }; ``` #### Message ```javascript const message = { subAccountId: "1867542890123456789", timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in SECONDS action: "websocket_auth" }; ``` **Important**: The `timestamp` field must be a Unix timestamp **in seconds** (not milliseconds). The server validates that timestamps are within ±60 seconds of server time to prevent replay attacks. ### Implementation Examples #### JavaScript/TypeScript ```javascript import { ethers } from 'ethers'; class WebSocketAuthenticator { constructor(privateKey) { this.wallet = new ethers.Wallet(privateKey); this.address = this.wallet.address; } async createAuthSignature(subAccountId, timestamp) { const timestampSec = Math.floor((timestamp || Date.now()) / 1000); const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ], AuthMessage: [ { name: "subAccountId", type: "uint256" }, { name: "timestamp", type: "uint256" }, { name: "action", type: "string" } ] }; const message = { subAccountId: BigInt(subAccountId), timestamp: BigInt(timestampSec), action: "websocket_auth" }; // Sign the typed data const signature = await this.wallet._signTypedData(domain, types, message); // Create payload (EIP-712 structured data for the message parameter) const payload = { types, primaryType: "AuthMessage", domain, message: { subAccountId: `0x${BigInt(subAccountId).toString(16)}`, timestamp: `0x${BigInt(timestampSec).toString(16)}`, action: "websocket_auth" } }; return { message: JSON.stringify(payload), signature: signature // Raw hex string }; } async connectAndAuth(wsUrl, subAccountId) { return new Promise((resolve, reject) => { const ws = new WebSocket(wsUrl); ws.onopen = async () => { try { // Create auth signature and payload const { message, signature } = await this.createAuthSignature(subAccountId); // Send auth message const authMessage = { id: "auth-1", method: "auth", params: { message, // JSON-stringified EIP-712 payload signature // Raw hex signature string } }; ws.send(JSON.stringify(authMessage)); } catch (error) { reject(error); ws.close(); } }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.id === "auth-1") { if (message.error) { reject(new Error(message.error.message)); ws.close(); } else if (message.result?.status === "authenticated") { console.log('Authenticated successfully'); resolve(ws); } } }; ws.onerror = (error) => { reject(error); }; // Timeout after 30 seconds setTimeout(() => { reject(new Error('Authentication timeout')); ws.close(); }, 30000); }); } } // Usage async function connectToTrading() { const authenticator = new WebSocketAuthenticator(process.env.PRIVATE_KEY); const subAccountId = "1867542890123456789"; // Your subaccount ID try { const ws = await authenticator.connectAndAuth('wss://papi.synthetix.io/v1/ws/trade', subAccountId); console.log('Connected and authenticated'); // Now you can use the authenticated WebSocket return ws; } catch (error) { console.error('Authentication failed:', error); } } ``` #### Python ```python import json import time import asyncio import os import websockets from eth_account import Account from eth_account.messages import encode_structured_data class WebSocketAuthenticator: def __init__(self, private_key): self.account = Account.from_key(private_key) self.address = self.account.address def create_auth_signature(self, sub_account_id, timestamp=None): if timestamp is None: timestamp = int(time.time()) # Unix timestamp in SECONDS # EIP-712 structured data data = { "types": { "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"} ], "AuthMessage": [ {"name": "subAccountId", "type": "uint256"}, {"name": "timestamp", "type": "uint256"}, {"name": "action", "type": "string"} ] }, "primaryType": "AuthMessage", "domain": { "name": "Synthetix", "version": "1", "chainId": 1, "verifyingContract": "0x0000000000000000000000000000000000000000" }, "message": { "subAccountId": int(sub_account_id), "timestamp": timestamp, "action": "websocket_auth" } } # Encode and sign encoded = encode_structured_data(data) signed = self.account.sign_message(encoded) # Return params in correct format for WebSocket auth return { "message": json.dumps(data), # JSON-stringified EIP-712 typed data "signature": signed.signature.hex() # Raw hex signature string } async def connect_and_auth(self, ws_url, sub_account_id): async with websockets.connect(ws_url) as websocket: # Create auth signature and payload auth_params = self.create_auth_signature(sub_account_id) # Send auth message auth_message = { "id": "auth-1", "method": "auth", "params": auth_params } await websocket.send(json.dumps(auth_message)) # Wait for response response = await websocket.recv() message = json.loads(response) if message.get("error"): raise Exception(f"Auth failed: {message['error']['message']}") elif message.get("result", {}).get("status") == "authenticated": print("Authenticated successfully") return websocket raise Exception("Unexpected auth response") # Usage async def main(): authenticator = WebSocketAuthenticator(os.environ["PRIVATE_KEY"]) sub_account_id = "1867542890123456789" # Your subaccount ID try: ws = await authenticator.connect_and_auth("wss://papi.synthetix.io/v1/ws/trade", sub_account_id) # Use authenticated websocket except Exception as e: print(f"Authentication failed: {e}") asyncio.run(main()) ``` ### Authentication Response #### Success Response ```json { "id": "auth-1", "status": 200, "result": { "status": "authenticated", "sub_account_id": "12345" }, "error": null } ``` #### Error Response ```json { "id": "auth-1", "status": 401, "result": null, "error": { "code": 401, "message": "Authentication failed: Invalid signature" } } ``` ### Subaccount Trading After authentication, use `subAccountId` in trading operations to specify which account to trade on: ```javascript const orderMessage = { id: "order-1", method: "post", params: { action: "placeOrders", subAccountId: "1867542890123456789", // Which subaccount to trade on orders: [{ symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", price: "50000", triggerPrice: "", quantity: "0.1", reduceOnly: false, isTriggerMarket: false }] } }; ``` Requirements: * Authentication signature determines who is trading * System verifies on-chain delegation permissions automatically * `subAccountId` specifies which account to operate on ### Session Management #### Session Lifetime * Sessions expire after 24 hours * Re-authentication required after expiration * Connection drops do not invalidate session immediately #### Multiple Connections * Each connection requires separate authentication * Maximum 5 concurrent authenticated connections per address * Sessions are independent ### Implementation Notes #### Timestamp Management Authentication timestamps must be in seconds and within ±60 seconds of server time: ```javascript class TimestampManager { constructor() { this.lastTimestamp = 0; } generateTimestamp() { const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in SECONDS // Ensure always increasing this.lastTimestamp = Math.max(timestamp, this.lastTimestamp + 1); return this.lastTimestamp; } } ``` **Note**: The server enforces a ±60 second window for timestamps to prevent replay attacks. #### Authentication Timeout WebSocket authentication must complete within 30 seconds: ```javascript async function authenticateWithTimeout(ws, authMessage, timeoutMs = 30000) { return Promise.race([ sendAuthMessage(ws, authMessage), new Promise((_, reject) => setTimeout(() => reject(new Error('Auth timeout')), timeoutMs) ) ]); } ``` ### Required Headers All WebSocket connections require a `User-Agent` header identifying your application: ```javascript const ws = new WebSocket('wss://papi.synthetix.io/v1/ws/trade', { headers: { 'User-Agent': 'my-trading-bot/1.0.0' } }); ``` Example user agent strings: * `my-trading-bot/1.0.0` * `my-company-name/arbitrage-service` * `my-app-name (contact@example.com)` ### Common Issues #### Invalid Signature **Causes:** * Wrong domain parameters (must use name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000") * Incorrect message structure (missing subAccountId, timestamp, or action) * Timestamp not in seconds or outside ±60 second window * Key mismatch **Solution:** ```javascript // Debug signature console.log('Domain:', domain); console.log('Types:', types); console.log('Message:', message); console.log('Signer address:', wallet.address); ``` #### Authentication Timeout **Cause:** Message not sent within 30 seconds of connection **Solution:** ```javascript ws.onopen = async () => { // Send auth immediately const authMessage = createAuthMessage(); ws.send(JSON.stringify(authMessage)); }; ``` #### Subaccount Access Errors **Cause:** No permission to trade on specified subaccount **Solution:** 1. Verify on-chain delegation permissions 2. Check that signer has authority for the target subaccount 3. Ensure subaccount ID is correct ### Testing Authentication ```javascript // Test authentication separately async function testAuth() { const testAuth = new WebSocketAuthenticator(TEST_PRIVATE_KEY); const testSubAccountId = "1867542890123456789"; try { // Test signature generation const signature = await testAuth.createAuthSignature(testSubAccountId); console.log('Signature generated:', signature); // Test connection const ws = await testAuth.connectAndAuth('wss://api.test.synthetix.io/v1/ws/trade', testSubAccountId); console.log('Test auth successful'); ws.close(); } catch (error) { console.error('Test auth failed:', error); } } ``` ### Next Steps * [Place Order](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Start trading after auth * [Connection Management](https://developers.synthetix.io/developer-resources/api/ws-api/timeouts-heartbeats) - Keep connection alive * [Authentication Guide](https://developers.synthetix.io/developer-resources/api/authentication) - Complete authentication documentation * [EIP-712 Signing](https://developers.synthetix.io/developer-resources/api/authentication/eip-712-signing) - Detailed signing implementation * [Nonce Management](https://developers.synthetix.io/developer-resources/api/authentication/nonce-management) - Advanced nonce strategies import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Cancel All Orders (WebSocket) Cancel all open orders for specific market(s), or all markets with the wildcard symbol, through the WebSocket connection. ### Action Result Notifications This method executes cancellations immediately. Cancellation events are automatically sent via [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) if subscribed. ### Request #### Request Format ```json { "id": "cancelall-1", "method": "post", "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["BTC-USDT", "ETH-USDT"], "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"cancelAllOrders"` | | `subAccountId` | string | Yes | SubAccount ID to cancel orders for | | `symbols` | array | Yes | Array of symbols to cancel. Use `["*"]` to cancel across all markets. `[]` is rejected. `["*", "ETH-USDT"]` is rejected | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration Unix timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature with v, r, s components | #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { CancelAllOrders: [ { name: "subAccountId", type: "uint256" }, { name: "symbols", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", symbols: ["BTC-USDT", "ETH-USDT"], nonce: Date.now(), expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` #### Wildcard Rules for `symbols` * `["*"]`: Cancel all open orders across all markets * `["BTC-USDT", "ETH-USDT"]`: Cancel only those markets * `[]`: Rejected * `["*", "ETH-USDT"]`: Rejected (wildcard must be used alone) ### Response Format #### Success Response ```json { "id": "cancelall-1", "status": 200, "result": { "status": "success", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "message": "", "symbol": "BTC-USDT" }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "message": "", "symbol": "ETH-USDT" } ] } } ``` #### Empty Result (No Orders to Cancel) ```json { "id": "cancelall-1", "status": 200, "result": { "status": "success", "response": [] } } ``` #### Error Response ```json { "id": "cancelall-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid request parameters" } } ``` ### Code Examples **Migration Note**: Use `order.venueId` as canonical in cancellation responses. `orderId` is deprecated compatibility output. The `message` field contains an error description when cancellation fails for a specific order, and is empty on success. Do not treat a non-empty `message` as a success confirmation. #### Cancel Orders for Specific Markets ```json { "id": "cancelall-markets", "method": "post", "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["BTC-USDT", "ETH-USDT"], "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` #### Cancel Orders for All Markets ```json { "id": "cancelall-all", "method": "post", "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["*"], "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` #### Cancel Orders for Single Market ```json { "id": "cancelall-btc", "method": "post", "params": { "action": "cancelAllOrders", "subAccountId": "1867542890123456789", "symbols": ["BTC-USDT"], "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Implementation Example ```javascript class OrderCancellation { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async cancelAllOrders(subAccountId, symbols) { if (!symbols || symbols.length === 0) { throw new Error('symbols array must contain at least one market symbol'); } if (symbols.includes('*') && symbols.length > 1) { throw new Error('wildcard "*" must be used alone as ["*"]'); } const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 30; // 30 seconds from now (Unix seconds) // Create EIP-712 signature const signature = await this.signCancelAllOrders({ subAccountId, symbols, nonce, expiresAfter }); const request = { id: `cancelall-${Date.now()}`, method: "post", params: { action: "cancelAllOrders", subAccountId, symbols, nonce, expiresAfter, signature } }; return this.sendRequest(request); } async signCancelAllOrders({ subAccountId, symbols, nonce, expiresAfter }) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { CancelAllOrders: [ { name: "subAccountId", type: "uint256" }, { name: "symbols", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId, symbols, nonce, expiresAfter }; const signature = await this.signer._signTypedData(domain, types, message); const { v, r, s } = ethers.utils.splitSignature(signature); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result); } } } } // Usage Examples // Cancel all orders for specific markets async function cancelByMarkets(cancellation, subAccountId, symbols) { try { const result = await cancellation.cancelAllOrders(subAccountId, symbols); console.log(`Cancelled ${result.response.length} orders in ${symbols.join(', ')}`); return result; } catch (error) { console.error('Cancel by markets failed:', error); throw error; } } // Cancel all BTC orders async function cancelAllBtcOrders(cancellation, subAccountId) { return cancelByMarkets(cancellation, subAccountId, ['BTC-USDT']); } ``` ### Cancel Behavior * **Immediate Effect**: Orders are cancelled immediately upon validation * **Margin Release**: Freed margin becomes available instantly * **Partial Fills**: Partially filled orders are cancelled for remaining quantity * **All Order Types**: Cancels limit, market, and trigger orders * **Atomic Operation**: All specified orders are cancelled together ### Implementation Details The API performs the following steps: 1. Validates that the `symbols` array contains at least one market 2. Validates wildcard usage (`*` may only appear as `["*"]`) 3. Retrieves all open orders for the authenticated subaccount 4. Filters orders by the specified symbols (or all when `["*"]` is used) 5. Iterates through each order and attempts to cancel it 6. Returns an array of cancellation statuses for each processed order ### Implementation Notes * `symbols` array must contain at least one valid market symbol * `symbols: ["*"]` cancels all markets and wildcard cannot be mixed with other symbols * Market symbols must be valid trading pairs (e.g., "BTC-USDT") * Authentication timestamps must be strictly increasing per subaccount * EIP-712 domain separator must use "Synthetix" for WebSocket endpoints * `expiresAfter` uses Unix seconds (not milliseconds) ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * `symbols` array must contain at least one valid market symbol * `["*"]` cancels all markets and cannot be combined with other symbols * `[]` is invalid * Must be signed by account owner or authorized delegate * Subaccount ID must be valid and accessible | Error | Description | | ------------------------- | ----------------------------------- | | Symbols must be non-empty | `symbols` array cannot be empty | | Invalid wildcard usage | `*` must be used alone as `["*"]` | | Invalid market symbol | Market symbol not recognized | | No orders found | No open orders to cancel | | Request expired | `expiresAfter` timestamp has passed | ### Next Steps * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Cancel specific orders by ID * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Query order status * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/cancelAllOrders) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Cancel Orders (WebSocket) Cancel one or more existing orders through the WebSocket connection for fastest order management. ### Action Result Notifications This method executes cancellations immediately. Cancellation events are automatically sent via [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) if subscribed. ### Request #### Request Format Cancel by venue order ID: ```json { "id": "cancel-1", "method": "post", "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "orderIds": ["1948058938469519360", "1948058938469519361"], "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", "s": "0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890" } } } ``` Cancel by client order ID (CLOID): ```json { "id": "cancel-2", "method": "post", "params": { "action": "cancelOrders", "subAccountId": "1867542890123456789", "clientOrderIds": ["my-order-alpha", "my-order-beta"], "nonce": 1704067200001, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", "s": "0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890" } } } ``` **Note**: `orderIds` and `clientOrderIds` are mutually exclusive. Providing both in the same request returns a validation error. ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | ---------------- | ------- | ----------- | ------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"cancelOrders"` | | `subAccountId` | string | Yes | Subaccount ID to cancel orders from | | `orderIds` | array | Conditional | Array of venue order IDs to cancel (strings). Mutually exclusive with `clientOrderIds`. | | `clientOrderIds` | array | Conditional | Array of client order IDs (CLOIDs) to cancel (strings). Mutually exclusive with `orderIds`. | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration Unix timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature with v, r, s components | Exactly one of `orderIds` or `clientOrderIds` must be provided. #### EIP-712 Type Definition **Cancel by venue order ID** (`orderIds` mode) — uses primary type `CancelOrders`: ```typescript const CancelOrdersTypes = { CancelOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orderIds", type: "uint256[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` **Note**: `orderIds` is `uint256[]` in EIP-712, but sent as `string[]` in the JSON request body. **Cancel by client order ID** (`clientOrderIds` mode) — uses primary type `CancelOrdersByCloid`: ```typescript const CancelOrdersByCloidTypes = { CancelOrdersByCloid: [ { name: "subAccountId", type: "uint256" }, { name: "clientOrderIds", type: "string[]" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` `clientOrderIds` is `string[]` in both the EIP-712 type and the JSON request body. The EIP-712 primary type must match the cancel mode used. ### Response Format #### Success Response ```json { "id": "cancel-1", "status": 200, "result": { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } }, { "canceled": { "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "id": "1948058938469519361" } } ] }, "requestId": "5ccf215d37e3ae6d" } } ``` #### Partial Success Response If some orders fail to cancel, the per-item status uses `error` / `errorCode` instead of `canceled`: ```json { "id": "cancel-1", "status": 200, "result": { "status": "ok", "response": { "statuses": [ { "canceled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } }, { "error": "Order not found", "errorCode": "ORDER_NOT_FOUND" } ] }, "requestId": "5ccf215d37e3ae6d" } } ``` **Migration Note**: In each status object, use `canceled.order.venueId` as canonical. `canceled.id` is deprecated compatibility output. #### Error Response ```json { "id": "cancel-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid request parameters" } } ``` ### Implementation Example ```javascript // Cancel single order const subAccountId = "1867542890123456789"; const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 30; // 30 seconds from now // Create EIP-712 signature (see EIP-712 Signing section below) const signature = await signCancelOrders({ subAccountId, orderIds: ["1948058938469519360"], nonce, expiresAfter }); const cancelRequest = { id: "cancel-1", method: "post", params: { action: "cancelOrders", subAccountId, orderIds: ["1948058938469519360"], nonce, expiresAfter, signature } }; ws.send(JSON.stringify(cancelRequest)); // Cancel multiple orders const cancelMultiple = { id: "cancel-2", method: "post", params: { action: "cancelOrders", subAccountId, orderIds: ["1948058938469519360", "1948058938469519361", "1948058938469519362"], nonce: Date.now(), expiresAfter: Math.floor(Date.now() / 1000) + 30, signature: await signCancelOrders({ subAccountId, orderIds: ["1948058938469519360", "1948058938469519361", "1948058938469519362"], nonce: Date.now(), expiresAfter: Math.floor(Date.now() / 1000) + 30 }) } }; ws.send(JSON.stringify(cancelMultiple)); // Handle response ws.onmessage = (event) => { const response = JSON.parse(event.data); if (response.id === "cancel-1") { if (response.result?.status === "ok") { response.result.response.statuses.forEach(status => { if (status.canceled) { const venueId = status.canceled.order?.venueId; console.log(`Order ${venueId}: canceled`); } else if (status.error) { console.error(`Order error: ${status.error} (${status.errorCode})`); } }); } else if (response.error) { console.error(`Request error: ${response.error.message}`); } } }; ``` ### Implementation Notes * Exactly one of `orderIds` or `clientOrderIds` must be provided per request * The provided array must contain at least one entry * Client order IDs must not have leading or trailing whitespace * Orders must belong to the authenticated subaccount * Authentication timestamps must be strictly increasing per subaccount * EIP-712 domain separator must use "Synthetix" for WebSocket endpoints * When using `clientOrderIds`, the EIP-712 primary type must be `CancelOrdersByCloid` (not `CancelOrders`) ### Cancellation Behavior * Only `OPEN` and `PARTIALLY_FILLED` orders can be cancelled * Filled portions of partially filled orders remain executed * Cancellation is immediate upon validation * Freed margin becomes available instantly ### Next Steps * [Modify Order](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/modifyOrder) - Order modification via WebSocket * [Cancel All Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelAllOrders) - Emergency cancellation * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/cancelOrders) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Create Subaccount (WebSocket) Create a new trading subaccount under the authenticated wallet address through the WebSocket connection. Subaccounts allow for isolated trading strategies, separate margin management, and organized position tracking within a single master account. ### Request #### Request Format ```json { "id": "create-subaccount-1", "method": "post", "params": { "action": "createSubaccount", "name": "Trading Bot Strategy", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------ | | `action` | string | Yes | Must be `"createSubaccount"` | | `name` | string | No | Optional display name for the new subaccount (max 50 chars) | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature for wallet authentication | **Important**: This endpoint must be sent over a WebSocket connection already authenticated to an owned subaccount. The EIP-712 payload signs that authenticated subaccount as `masterSubAccountId` to prove ownership before creating the new subaccount. #### Naming Guidelines * **Optional Parameter**: Name can be omitted for system-generated naming * **Character Limit**: Maximum 50 characters when specified * **Default Behavior**: If no name provided, system may generate a default identifier Examples: "Main Trading", "DCA Strategy", "Arbitrage Bot", "Risk Management" ### Response Format #### Success Response (New Account) When a subaccount is first created, `subAccountId`, `subAccountName`, and `accountLimits` are populated. All other fields return zero/null values: ```json { "id": "create-subaccount-1", "status": 200, "result": { "subAccountId": "1867542890123456790", "masterAccountId": null, "subAccountName": "Trading Bot Strategy", "collaterals": null, "crossMarginSummary": { "accountValue": "", "availableMargin": "", "totalUnrealizedPnl": "", "maintenanceMargin": "", "initialMargin": "", "withdrawable": "", "adjustedAccountValue": "", "debt": "" }, "positions": null, "marketPreferences": { "leverages": null }, "feeRates": { "makerFeeRate": "", "takerFeeRate": "", "tierName": "" }, "accountLimits": { "maxBorrowCapacity": "100000", "maxOrdersPerMarket": 50, "maxSubAccounts": 10, "maxTotalOrders": 200 } } } ``` #### Response Fields | Field | Type | Description | | --------------------------------------- | -------------- | --------------------------------------------------------------------------- | | `subAccountId` | string | System-generated unique identifier for the new subaccount | | `masterAccountId` | string \| null | Master account ID (returns `null`) | | `subAccountName` | string | Display name provided in request (or empty if not specified) | | `collaterals` | array \| null | Collateral balances (returns `null` for new accounts) | | `collaterals[].adjustedCollateralValue` | string | Adjusted collateral value after applying haircuts | | `collaterals[].calculatedAt` | integer | Unix millisecond timestamp of the collateral price calculation (0 for USDT) | | `collaterals[].collateralValue` | string | Raw collateral value | | `collaterals[].haircutRate` | string | Haircut rate applied to the collateral | | `collaterals[].haircutAdjustment` | string | Calculated haircut adjustment amount | | `collaterals[].pendingWithdraw` | string | Amount pending withdrawal | | `collaterals[].price` | string | Collateral price used for valuation | | `collaterals[].quantity` | string | Total collateral quantity held | | `collaterals[].symbol` | string | Collateral asset symbol | | `collaterals[].withdrawable` | string | Amount available to withdraw | | `crossMarginSummary` | object | Cross-margin account summary (returns empty strings for new accounts) | | `crossMarginSummary.debt` | string | USDT debt after positive unrealized PnL offset | | `positions` | array \| null | Open positions (returns `null` for new accounts) | | `marketPreferences` | object | Market-specific preferences like leverage | | `feeRates` | object | Fee rate information | | `accountLimits` | object | Account limit information | | `accountLimits.maxBorrowCapacity` | string | Maximum borrow capacity in USDT | | `accountLimits.maxOrdersPerMarket` | integer | Maximum open orders allowed per market | | `accountLimits.maxSubAccounts` | integer | Maximum number of subaccounts allowed for the wallet | | `accountLimits.maxTotalOrders` | integer | Maximum total open orders across all markets | #### Error Response ```json { "id": "create-subaccount-1", "status": 400, "result": null, "error": { "code": 400, "message": "Subaccount limit reached" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function createSubaccount(ws, signer, masterSubAccountId, name = "") { const nonce = Date.now(); const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { CreateSubaccount: [ { name: "masterSubAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const expiresAfter = 0; // Set to a future timestamp to enforce expiry, or 0 for no expiry const message = { masterSubAccountId: BigInt(masterSubAccountId), name, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); ws.send(JSON.stringify({ id: `create-subaccount-${Date.now()}`, method: "post", params: { action: "createSubaccount", name, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Create a new trading subaccount after authenticating the WebSocket as the owner await createSubaccount(ws, signer, "1867542890123456789", "Grid Trading Bot"); ``` ### Code Examples #### Create Basic Subaccount ```json { "id": "create-basic", "method": "post", "params": { "action": "createSubaccount", "name": "Main Trading", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Create Strategy-Specific Subaccount ```json { "id": "create-strategy", "method": "post", "params": { "action": "createSubaccount", "name": "Grid Trading Bot", "nonce": 1735689600001, "expiresAfter": 1735689901, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Create Subaccount Without Name ```json { "id": "create-unnamed", "method": "post", "params": { "action": "createSubaccount", "nonce": 1735689600002, "expiresAfter": 1735689902, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Notes * **Ownership Proof**: Authenticate the WebSocket as an owned subaccount, then sign that authenticated subaccount as `masterSubAccountId` in the EIP-712 payload * **Immediate Availability**: Subaccount is immediately available for trading after creation * **Account Limits**: Platform may enforce maximum number of subaccounts per wallet * **Initial State**: New subaccounts have zero balances and no positions * **Security Inheritance**: Each subaccount inherits master wallet security settings ### Common Errors | Error Code | Message | Description | | ---------- | ------------------------ | --------------------------------------------------------- | | 400 | Subaccount limit reached | Maximum subaccounts for this wallet exceeded | | 400 | Invalid subaccount name | Name exceeds 50 characters or contains invalid characters | | 401 | Authentication failed | Invalid signature | ### Next Steps * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Retrieve account details * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Delegate trading access * [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Start trading * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/createSubaccount) - REST API comparison import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import WsTradeEndpoints from "../../../../../snippets/ws-trade-endpoints.mdx"; ## Get Balance Updates (WebSocket) Retrieve historical deposit, withdrawal, and transfer transactions for a subaccount through the WebSocket connection. This endpoint returns balance changes from on-chain deposits, withdrawals, and internal transfers, excluding funding payments. ### Request #### Request Format ```json { "id": "balance-updates-1", "method": "post", "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "DEPOSIT", "limit": 50, "offset": 0, "startTime": 1704067200000, "endTime": 1704153600000, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getBalanceUpdates"` | | `subAccountId` | string | Yes | Subaccount identifier | | `actionFilter` | string | No | Filter by action type: `"DEPOSIT"`, `"WITHDRAWAL"`, `"TRANSFER"`, or comma-separated (e.g. `"DEPOSIT,WITHDRAWAL"`). Defaults to all types | | `limit` | integer | No | Maximum number of results to return (default: 50, max: 1000) | | `offset` | integer | No | Pagination offset (default: 0, max: 10000) | | `startTime` | integer | No | Start of time range as Unix timestamp in milliseconds. Defaults to 7 days before `endTime` (or 7 days ago if neither is set) | | `endTime` | integer | No | End of time range as Unix timestamp in milliseconds. Defaults to 7 days after `startTime` (or now if neither is set). Maximum range is 365 days | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * Returns deposit, withdrawal, and internal transfer transactions (excludes funding payments) * For funding payment history, use [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getFundingPayments) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" }, ], }; const message = { subAccountId: "1867542890123456789", action: "getBalanceUpdates", expiresAfter: 0, }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "balance-updates-1", "status": 200, "result": { "balanceUpdates": [ { "id": "12345", "subAccountId": "1867542890123456789", "action": "DEPOSIT", "status": "success", "amount": "1000.00", "fee": "0.00", "grossAmount": "1000.00", "collateral": "USDT", "timestamp": 1704067200000 }, { "id": "12346", "subAccountId": "1867542890123456789", "action": "WITHDRAWAL", "status": "success", "amount": "-500.00", "fee": "5.00", "grossAmount": "-495.00", "collateral": "USDT", "timestamp": 1704153600000, "destinationAddress": "0xabcdef1234567890abcdef1234567890abcdef12", "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } ] } } ``` #### Empty Result ```json { "id": "balance-updates-1", "status": 200, "result": { "balanceUpdates": [] } } ``` #### Error Response ```json { "id": "balance-updates-1", "status": 400, "result": null, "error": { "code": 400, "message": "limit cannot exceed 1000" } } ``` ### Response Fields #### Balance Update Object | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` | string | Unique transaction ID (string for JS BigInt compatibility) | | `subAccountId` | string | Subaccount ID associated with the transaction (string for JS BigInt compatibility) | | `action` | string | Action type: `"DEPOSIT"`, `"WITHDRAWAL"`, or `"TRANSFER"` | | `status` | string | Transaction status: `"pending"`, `"success"`, or `"failed"` | | `amount` | string | Net amount changed (positive for deposits, negative for withdrawals) | | `fee` | string | Fee charged for the transaction | | `grossAmount` | string | Total amount including fee (`amount + fee`) | | `collateral` | string | Collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `timestamp` | integer | Unix timestamp in milliseconds when the transaction was created | | `destinationAddress` | string | Destination address for withdrawals (optional, only for withdrawals) | | `txHash` | string | On-chain transaction hash (optional) | | `fromSubAccountId` | string | Source subaccount ID for internal transfers (optional) | | `toSubAccountId` | string | Target subaccount ID for internal transfers (optional) | ### Code Examples #### Get All Balance Updates ```json { "id": "all-updates", "method": "post", "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Deposits Only ```json { "id": "deposits-only", "method": "post", "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "DEPOSIT", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Withdrawals Only ```json { "id": "withdrawals-only", "method": "post", "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "actionFilter": "WITHDRAWAL", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Paginated Request ```json { "id": "paginated", "method": "post", "params": { "action": "getBalanceUpdates", "subAccountId": "1867542890123456789", "limit": 100, "offset": 100, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class BalanceUpdatesQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getBalanceUpdates(options = {}) { const { subAccountId, actionFilter = null, limit = 50, offset = 0, expiresAfter = 0, } = options; const signature = await this.signSubAccountAction( subAccountId, "getBalanceUpdates", expiresAfter, ); const request = { id: `balance-updates-${Date.now()}`, method: "post", params: { action: "getBalanceUpdates", subAccountId, limit, offset, expiresAfter, signature, }, }; if (actionFilter) { request.params.actionFilter = actionFilter; } return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" }, ], }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter), }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error("Request timeout")); } }, 10000); }); } } // Usage const query = new BalanceUpdatesQuery(ws, signer); // Get all deposits const deposits = await query.getBalanceUpdates({ subAccountId: "1867542890123456789", actionFilter: "DEPOSIT", }); console.log(`Found ${deposits.balanceUpdates.length} deposits`); ``` ### Implementation Notes * Returns deposit, withdrawal, and internal transfer transactions (excludes funding payments) * Results are ordered by timestamp (most recent first) * Default limit is 50 results per request, maximum is 1000 * Pagination offset maximum is 10,000 * Use pagination with `limit` and `offset` for large result sets * When `startTime` and `endTime` are both omitted, the default time window is the last 7 days * When only one time bound is supplied, the other is automatically extended by 7 days (end time is capped at now) * Maximum allowed time range is 365 days; requests spanning a longer period are rejected * Authentication requires signature by account owner or authorized delegate ### Common Errors | Error Code | Message | Description | | ---------- | ----------------------------------------------------------------------- | ------------------------------- | | 400 | subAccountId is required | No subaccount ID was provided | | 400 | limit cannot exceed 1000 | Limit parameter exceeds maximum | | 400 | limit must be non-negative | Negative limit value provided | | 400 | offset must be non-negative | Negative offset value provided | | 400 | offset cannot exceed 10000 | Offset exceeds maximum of 10000 | | 400 | invalid action filter, available filters: DEPOSIT, WITHDRAWAL, TRANSFER | Invalid action filter value | ### Next Steps * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - View current account balances * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getFundingPayments) - Funding payment history * [Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/withdrawCollateral) - Initiate withdrawals * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getBalanceUpdates) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Delegated Signers (WebSocket) Retrieve a list of all delegated signers for a specific subaccount through the WebSocket connection. This endpoint provides visibility into which wallet addresses have been granted permissions to act on behalf of the subaccount, along with their permission levels and delegation details. ### Request #### Request Format ```json { "id": "delegated-signers-1", "method": "post", "params": { "action": "getDelegatedSigners", "subAccountId": "1867542890123456789", "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------- | | `action` | string | Yes | Must be `"getDelegatedSigners"` | | `subAccountId` | string | Yes | Subaccount identifier | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * Both master account owners and delegated signers can view the delegation list #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getDelegatedSigners", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "delegated-signers-1", "status": 200, "result": { "delegatedSigners": [ { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "1867542890123456789", "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "permissions": ["delegate"], "expiresAt": 1767225600000 } ] } } ``` #### Empty Response ```json { "id": "delegated-signers-1", "status": 200, "result": { "delegatedSigners": [] } } ``` #### Error Response ```json { "id": "delegated-signers-1", "status": 404, "result": null, "error": { "code": 404, "message": "Subaccount not found" } } ``` ### Response Fields | Field | Type | Description | | ------------------ | ----- | --------------------------------- | | `delegatedSigners` | array | Array of delegated signer objects | #### Delegate Object ### Code Examples #### Get All Delegated Signers ```json { "id": "get-delegates", "method": "post", "params": { "action": "getDelegatedSigners", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class DelegationQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getDelegatedSigners(subAccountId, expiresAfter = 0) { const signature = await this.signSubAccountAction(subAccountId, "getDelegatedSigners", expiresAfter); const request = { id: `delegated-signers-${Date.now()}`, method: "post", params: { action: "getDelegatedSigners", subAccountId, expiresAfter, signature } }; return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } } // Usage const query = new DelegationQuery(ws, signer); const result = await query.getDelegatedSigners("1867542890123456789"); console.log(`Found ${result.delegatedSigners.length} delegated signers`); result.delegatedSigners.forEach(delegate => { console.log(`Address: ${delegate.walletAddress}`); console.log(`Permissions: ${delegate.permissions.join(', ')}`); console.log(`Expires: ${delegate.expiresAt || 'Never'}`); }); ``` ### Use Cases * **Team Management**: View all team members with access to a trading account * **Security Audit**: Regular review of who has access to perform operations * **Access Control UI**: Build interfaces showing current permissions * **Bot Management**: Track which automated systems have trading access * **Compliance**: Maintain records of account access for regulatory purposes ### Implementation Notes * **Access Control**: Both master account owners and delegated signers can view the delegation list * **Real-time Data**: Returns current active delegations only * **Expiration Handling**: Expired delegations are automatically excluded from results * **No Pagination**: All delegated signers are returned in a single response ### Common Errors | Error Code | Message | Description | | ---------- | --------------------- | --------------------- | | 404 | Subaccount not found | Invalid subaccount ID | | 401 | Authentication failed | Invalid signature | ### Next Steps * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Add new delegation * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/removeDelegatedSigner) - Revoke delegation * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Account details * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegatedSigners) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Delegations For Delegate (WebSocket) Retrieve a list of all delegations granted to the authenticated wallet address through the WebSocket connection. This endpoint allows a delegate wallet to discover which accounts have delegated permissions to it, enabling frontends to detect when a connected wallet is a delegate and allow switching to the delegator's account. ### Request #### Request Format ```json { "id": "delegations-for-delegate-1", "method": "post", "params": { "action": "getDelegationsForDelegate", "subAccountId": "1867542890123456789", "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getDelegationsForDelegate"` | | `subAccountId` | string | Yes | Subaccount ID included in the signed `SubAccountAction` payload | | `owningAddress` | string | No | Optional Ethereum address to query on behalf of. When provided and different from the signing wallet, the caller must have `trading` permission for that address | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature object | \::::info Authentication The delegate address is determined from the wallet that signed the request. By default, this returns delegations for the signing wallet. If you provide `owningAddress`, the caller must have `trading` permission for that address. \:::: #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt("1867542890123456789"), action: "getDelegationsForDelegate", expiresAfter: 0 }; const signature = await signer.signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "delegations-for-delegate-1", "status": 200, "result": { "delegatedAccounts": [ { "subAccountId": "1867542890123456789", "ownerAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "accountName": "Main Trading Account", "accountValue": "12500.50", "adjustedAccountValue": "11875.47", "permissions": ["session"], "expiresAt": null }, { "subAccountId": "2987654321098765432", "ownerAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "accountName": "Secondary Account", "accountValue": "4300.00", "adjustedAccountValue": "4085.00", "permissions": ["delegate"], "expiresAt": 1767225600000 } ] } } ``` #### Empty Response ```json { "id": "delegations-for-delegate-1", "status": 200, "result": { "delegatedAccounts": [] } } ``` #### Error Response ```json { "id": "delegations-for-delegate-1", "status": 401, "result": null, "error": { "code": 401, "message": "Authentication failed" } } ``` ### Response Fields | Field | Type | Description | | ------------------- | ----- | ---------------------------------- | | `delegatedAccounts` | array | Array of delegated account objects | #### Delegated Account Object | Field | Type | Description | | ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `subAccountId` | string | Subaccount ID this delegation applies to | | `ownerAddress` | string | Ethereum wallet address of the account owner who granted the delegation | | `accountName` | string | Human-readable name of the delegated account | | `accountValue` | string | Total account value of the delegating subaccount in USDT including unrealized P\&L | | `adjustedAccountValue` | string | Haircut-adjusted collateral value plus unrealized P\&L, in USDT | | `permissions` | string\[] | Array of permission levels granted. Current responses typically return `["session"]` or `["delegate"]`; legacy records may still show `["trading"]` | | `expiresAt` | integer \| null | Unix timestamp (milliseconds) when the delegation expires. `null` indicates no expiration | ### Code Examples #### Get All Delegations for Connected Wallet ```json { "id": "get-my-delegations", "method": "post", "params": { "action": "getDelegationsForDelegate", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class DelegationQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getDelegationsForDelegate(subAccountId, expiresAfter = 0, owningAddress) { const signature = await this.signDelegateAction(subAccountId, "getDelegationsForDelegate", expiresAfter); const request = { id: `delegations-for-delegate-${Date.now()}`, method: "post", params: { action: "getDelegationsForDelegate", subAccountId, expiresAfter, signature, ...(owningAddress ? { owningAddress } : {}) } }; return this.sendRequest(request); } async signDelegateAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } } // Usage - Detect if connected wallet is a delegate const query = new DelegationQuery(ws, signer); const result = await query.getDelegationsForDelegate("1867542890123456789"); if (result.delegatedAccounts.length > 0) { console.log(`This wallet is a delegate for ${result.delegatedAccounts.length} account(s):`); result.delegatedAccounts.forEach(account => { console.log(` Account: ${account.accountName}`); console.log(` Owner: ${account.ownerAddress}`); console.log(` Value: ${account.accountValue} USDT`); console.log(` SubAccount ID: ${account.subAccountId}`); console.log(` Permissions: ${account.permissions.join(', ')}`); console.log(` Expires: ${account.expiresAt ? new Date(account.expiresAt).toISOString() : 'Never'}`); console.log('---'); }); } else { console.log('This wallet is not a delegate for any accounts'); } ``` ### Use Cases * **Wallet Detection**: Frontend can detect when a connected wallet is a delegate and offer account switching * **Account Switching UI**: Build interfaces allowing delegates to switch between delegated accounts * **Multi-Account Management**: Delegates managing multiple accounts can see all their delegations in one place * **Access Verification**: Confirm which accounts a delegate has permission to trade on * **Team Dashboard**: Team members can see which accounts they have access to ### Implementation Notes * **Delegate-Centric Query**: Unlike `getDelegatedSigners` which queries by subaccount, this endpoint returns all accounts that have delegated permissions to the authenticated wallet * **Access Control**: Returns delegations for the signing wallet by default, or for `owningAddress` when the caller has `trading` permission for that address * **Real-time Data**: Returns current active delegations only * **Expiration Handling**: Expired delegations are automatically excluded from results * **No Pagination**: All delegations are returned in a single response * **Owner Information**: Includes the `ownerAddress`, `accountName`, `accountValue`, and `adjustedAccountValue` to help identify which accounts have granted delegation. `adjustedAccountValue` reflects haircut-adjusted collateral plus unrealized P\&L * **Permission Values**: New session-level grants are stored and returned as `session`; legacy records may still appear as `trading` ### Common Errors | Error Code | Message | Description | | ---------- | --------------------- | --------------------------------------------------------------------------- | | 400 | Validation error | `owningAddress` is not a valid hex Ethereum address | | 401 | Authentication failed | Invalid signature | | 403 | Forbidden | Caller does not have `trading` permission for the specified `owningAddress` | ### Next Steps * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - Query delegations by subaccount * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Add new delegation * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/removeDelegatedSigner) - Revoke delegation * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Account details * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getDelegationsForDelegate) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Funding Payments (WebSocket) Retrieve detailed funding payment history and statistics for the authenticated subaccount through the WebSocket connection. This endpoint provides user-specific funding data including payments received, paid, and net funding across all positions. ### Request #### Request Format ```json { "id": "funding-payments-1", "method": "post", "params": { "action": "getFundingPayments", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "startTime": 1735603200000, "endTime": 1735689600000, "limit": 100, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getFundingPayments"` | | `subAccountId` | string | Yes | Subaccount identifier | | `symbol` | string | No | Filter by specific trading pair (e.g., `"BTC-USDT"`) | | `startTime` | integer | No | Start timestamp for filtering (milliseconds). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `endTime` | integer | No | End timestamp for filtering (milliseconds). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. | | `limit` | integer | No | Maximum number of records to return. Default: 100. Maximum: 1000. | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * For deposit/withdrawal history, use [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getBalanceUpdates) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getFundingPayments", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "funding-payments-1", "status": 200, "result": { "summary": { "totalFundingReceived": "125.75000000", "totalFundingPaid": "89.25000000", "netFunding": "36.50000000", "totalPayments": "247", "averagePaymentSize": "0.87044534" }, "fundingHistory": [ { "paymentId": "fp_1958787130134106112", "symbol": "BTC-USDT", "positionSize": "2.50000000", "fundingRate": "0.00001250", "payment": "-1.12500000", "timestamp": 1735689600000, "paymentTime": 1735689600000, "fundingTimestamp": 1735689600000, "fundingTime": 1735689600000 }, { "paymentId": "fp_1958787130134106111", "symbol": "ETH-USDT", "positionSize": "-5.00000000", "fundingRate": "-0.00002100", "payment": "3.15000000", "timestamp": 1735661200000, "paymentTime": 1735661200000, "fundingTimestamp": 1735661200000, "fundingTime": 1735661200000 } ] } } ``` #### Empty Result ```json { "id": "funding-payments-1", "status": 200, "result": { "summary": { "totalFundingReceived": "0", "totalFundingPaid": "0", "netFunding": "0", "totalPayments": "0", "averagePaymentSize": "0" }, "fundingHistory": [] } } ``` #### Error Response ```json { "id": "funding-payments-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid time range parameters" } } ``` ### Response Fields #### Summary Object | Field | Type | Description | | ---------------------- | ------ | ------------------------------------- | | `totalFundingReceived` | string | Total funding payments received | | `totalFundingPaid` | string | Total funding payments paid | | `netFunding` | string | Net funding (received - paid) | | `totalPayments` | string | Total number of funding payments | | `averagePaymentSize` | string | Average payment size (absolute value) | #### Funding History Object | Field | Type | Description | | ------------------ | ------- | -------------------------------------------- | | `paymentId` | string | Unique identifier for the funding payment | | `symbol` | string | Trading pair symbol | | `positionSize` | string | Position size at funding time (signed) | | `fundingRate` | string | Funding rate applied (1-hour rate) | | `payment` | string | Funding payment amount (negative = paid out) | | `timestamp` | integer | **DEPRECATED** - Use `paymentTime` instead | | `paymentTime` | integer | When payment was processed | | `fundingTimestamp` | integer | **DEPRECATED** - Use `fundingTime` instead | | `fundingTime` | integer | Funding period timestamp | #### Payment Calculation **Formula**: `Payment = Position Size × Funding Rate × Mark Price` * **Positive payment**: Funding received * **Negative payment**: Funding paid * **Long positions**: Pay when funding rate is positive * **Short positions**: Receive when funding rate is positive ### Code Examples #### Get All Funding Payments ```json { "id": "all-funding", "method": "post", "params": { "action": "getFundingPayments", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Symbol ```json { "id": "btc-funding", "method": "post", "params": { "action": "getFundingPayments", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Time Range ```json { "id": "time-range-funding", "method": "post", "params": { "action": "getFundingPayments", "subAccountId": "1867542890123456789", "startTime": 1735603200000, "endTime": 1735689600000, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class FundingPaymentsQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getFundingPayments(options = {}) { const { subAccountId, symbol = null, startTime = null, endTime = null, limit = 100, expiresAfter = 0 } = options; const signature = await this.signSubAccountAction(subAccountId, "getFundingPayments", expiresAfter); const request = { id: `funding-payments-${Date.now()}`, method: "post", params: { action: "getFundingPayments", subAccountId, limit, expiresAfter, signature } }; if (symbol) request.params.symbol = symbol; if (startTime) request.params.startTime = startTime; if (endTime) request.params.endTime = endTime; return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } } // Usage: Analyze funding performance const query = new FundingPaymentsQuery(ws, signer); const result = await query.getFundingPayments({ subAccountId: "1867542890123456789", startTime: Date.now() - (30 * 24 * 60 * 60 * 1000) // Last 30 days }); const netFunding = parseFloat(result.summary.netFunding); console.log(`Net funding last 30 days: ${netFunding} USDT`); ``` ### Use Cases #### Funding Performance Analysis ```javascript // Analyze funding performance over time const fundingData = await query.getFundingPayments({ subAccountId: "1867542890123456789", startTime: Date.now() - (30 * 24 * 60 * 60 * 1000) // Last 30 days }); const netFunding = parseFloat(fundingData.summary.netFunding); console.log(`Net funding last 30 days: ${netFunding} USDT`); ``` #### Position-Specific Funding ```javascript // Get funding for specific market const btcFunding = await query.getFundingPayments({ subAccountId: "1867542890123456789", symbol: "BTC-USDT" }); ``` ### Common Errors | Status | Error Code | Message | Solution | | ------ | ------------------ | ----------------------------------- | ---------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | subAccountId is required | Provide a valid authenticated subaccount | | 400 | `VALIDATION_ERROR` | invalid symbol | Use a valid symbol (e.g., `BTC-USDT`) from getMarkets | | 400 | `VALIDATION_ERROR` | startTime must be less than endTime | Ensure `startTime` is strictly less than `endTime` | | 400 | `VALIDATION_ERROR` | startTime must be a valid timestamp | Use a timestamp within the last year and not in the future | | 400 | `VALIDATION_ERROR` | limit must not exceed 1000 | Reduce the limit to 1000 or below | | 400 | `INVALID_FORMAT` | Invalid request body | Check request structure and field types | ### Next Steps * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Current positions * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getBalanceUpdates) - Deposit/withdrawal history * [Get Performance History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPerformanceHistory) - Overall performance * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getFundingPayments) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import OrderObject from '../../../../../snippets/order-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Open Orders (WebSocket) Retrieve all currently open orders for the authenticated subaccount through the WebSocket connection. This method returns only active orders (not filled or cancelled), with optional filtering by symbol and pagination. ### Request-Response vs Subscriptions This method provides **on-demand snapshots** of open order data. For **real-time updates** when orders change, use [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates). | Method | Purpose | When to Use | | -------------------------- | ----------------------- | ------------------------------------------ | | `getOpenOrders` | Active orders snapshot | Initial load, order management, UI refresh | | Order Updates Subscription | Real-time notifications | Live UI updates, event handling | ### Request #### Request Format ```json { "id": "openorders-1", "method": "post", "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "limit": 50, "offset": 0, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------------- | | `action` | string | Yes | Must be `"getOpenOrders"` | | `subAccountId` | string | Yes | Subaccount identifier | | `symbol` | string | No | Filter orders by specific market symbol (e.g., "BTC-USDT") | | `limit` | integer | No | Maximum number of orders to return (default: 50) | | `offset` | integer | No | Number of orders to skip for pagination (default: 0) | | `expiresAfter` | integer | No | Optional expiration timestamp in milliseconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * Returns only active orders (placed, partially filled) * Does not include filled, cancelled, or rejected orders #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getOpenOrders", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "openorders-1", "status": 200, "result": { "status": "success", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "symbol": "BTC-USDT", "side": "buy", "type": "LIMIT", "quantity": "0.1", "price": "45000.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": false, "closePosition": false, "createdTime": 1755846234000, "updatedTime": 1755846234000, "filledQuantity": "0.0", "takeProfitOrder": { "venueId": "1958787130134106115", "clientId": "cli-1958787130134106115" }, "takeProfitOrderId": "1958787130134106115", "stopLossOrder": { "venueId": "1958787130134106116", "clientId": "cli-1958787130134106116" }, "stopLossOrderId": "1958787130134106116" }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "symbol": "ETH-USDT", "side": "sell", "type": "LIMIT", "quantity": "2.0", "price": "2800.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": true, "closePosition": false, "createdTime": 1755846235000, "updatedTime": 1755846235000, "filledQuantity": "0.5" }, { "order": { "venueId": "1958787130134106120", "clientId": "cli-1958787130134106120" }, "orderId": "1958787130134106120", "symbol": "BTC-USDT", "side": "buy", "type": "limit", "quantity": "1.0", "price": "45000.00", "triggerPrice": "", "triggerPriceType": "", "timeInForce": "GTC", "reduceOnly": false, "postOnly": false, "closePosition": false, "createdTime": 1755846236000, "updatedTime": 1755846240000, "filledQuantity": "0.5", "expiresAt": 1755850000000 } ] } } ``` #### Empty Result ```json { "id": "openorders-1", "status": 200, "result": { "status": "success", "response": [] } } ``` #### Error Response ```json { "id": "openorders-1", "status": 400, "result": null, "error": { "code": 400, "message": "Failed to get open orders" } } ``` ### Order Object Fields | Field | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `order` | object | Canonical order identifier object | | `order.venueId` | string | Canonical venue-generated order identifier | | `order.clientId` | string | Optional client-provided order identifier | | `orderId` | string | Deprecated legacy order identifier | | `symbol` | string | Trading pair symbol (e.g., "BTC-USDT") | | `side` | string | Order side: `"buy"` or `"sell"` | | `type` | string | Order type (e.g., `"LIMIT"`, `"MARKET"`) | | `quantity` | string | Total order quantity | | `price` | string | Order price (empty for market orders) | | `triggerPrice` | string | Trigger price for conditional orders (populated for stop market, stop limit, take profit limit, take profit market; omitted for non-conditional orders) | | `triggerPriceType` | string | Reference price used to evaluate the trigger: `"mark_price"` or `"last_price"` | | `timeInForce` | string | Time in force: `"GTC"`, `"IOC"`, or `"FOK"` | | `reduceOnly` | boolean | Whether order can only reduce position | | `postOnly` | boolean | Whether order must be maker (no immediate match) | | `closePosition` | boolean | Whether order closes entire position | | `createdTime` | integer | Order creation timestamp (Unix milliseconds) | | `updatedTime` | integer | Last update timestamp (Unix milliseconds) | | `filledQuantity` | string | Quantity that has been filled | | `takeProfitOrder` | object | Canonical linked take-profit order identifier object | | `takeProfitOrderId` | string | Deprecated linked take-profit venue ID | | `stopLossOrder` | object | Canonical linked stop-loss order identifier object | | `stopLossOrderId` | string | Deprecated linked stop-loss venue ID | | `expiresAt` | integer | Order expiration timestamp (Unix milliseconds); omitted when the order has no explicit expiry | **Migration Note**: Use `order.venueId` as canonical. `orderId`, `takeProfitOrderId`, and `stopLossOrderId` are deprecated compatibility fields. **Note on TP/SL Fields**: When orders are placed with `normalTpsl` or `positionTpsl` grouping, the entry order will contain `takeProfitOrder` and `stopLossOrder` objects linking to associated TP/SL trigger orders. ### Code Examples #### Get All Open Orders ```json { "id": "openorders-all", "method": "post", "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Open Orders for Specific Market ```json { "id": "openorders-btc", "method": "post", "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Open Orders with Pagination ```json { "id": "openorders-paginated", "method": "post", "params": { "action": "getOpenOrders", "subAccountId": "1867542890123456789", "limit": 100, "offset": 0, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class OpenOrdersQuery { constructor(ws) { this.ws = ws; this.pendingRequests = new Map(); } async getOpenOrders(options = {}) { const { subAccountId, symbol = null, limit = 50, offset = 0, expiresAfter = 0, signature } = options; const request = { id: `openorders-${Date.now()}`, method: "post", params: { action: "getOpenOrders", subAccountId: subAccountId.toString(), expiresAfter, signature } }; // Add optional filters if (symbol) request.params.symbol = symbol; if (limit) request.params.limit = limit; if (offset) request.params.offset = offset; return this.sendRequest(request); } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result.response); } } } } // Usage const query = new OpenOrdersQuery(ws); const openOrders = await query.getOpenOrders({ subAccountId: "1867542890123456789", symbol: "BTC-USDT", signature: { v: 28, r: "0x...", s: "0x..." } }); console.log(`Found ${openOrders.length} open orders`); ``` ### Comparison with getOrderHistory | Feature | getOpenOrders | getOrderHistory | | -------------------- | ---------------------------- | ------------------------------------- | | **Purpose** | Currently active orders only | All orders with comprehensive history | | **Time Filtering** | Not supported | fromTime/toTime range | | **Status Filtering** | Not supported (active only) | Multiple status filters | | **Default Scope** | Active orders only | All orders (any status) | | **Use Case** | Real-time order management | Historical analysis and reporting | | **Performance** | Faster (smaller dataset) | May be slower with large history | ### Integration with Subscriptions Combine request-response queries with subscriptions for complete order management: ```javascript class OrderManager { constructor(ws, subAccountId) { this.ws = ws; this.subAccountId = subAccountId; this.openOrders = new Map(); } async initialize() { // 1. Get initial open orders const request = { id: "init-openorders", method: "post", params: { action: "getOpenOrders", subAccountId: this.subAccountId, expiresAfter: 0, signature: { /* ... */ } } }; this.ws.send(JSON.stringify(request)); // 2. Subscribe to real-time updates this.ws.send(JSON.stringify({ id: "subscribe-orders", method: "subscribe", params: { type: "subAccountUpdates", subAccountId: this.subAccountId } })); } handleOrderUpdate(update) { const { eventType } = update.data; const orderKey = update.data.order?.venueId || update.data.orderId; if (eventType === 'cancelled' || eventType === 'filled') { this.openOrders.delete(orderKey); } else if (eventType === 'placed' || eventType === 'partially_filled') { this.openOrders.set(orderKey, update.data); } } } ``` ### Validation Rules * Subaccount ID must be valid and accessible * Limit must be between 1 and 100 (default: 50) * Offset must be non-negative (default: 0) * Symbol must be a valid trading pair (if specified) ### Next Steps * [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time order change notifications * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Query historical orders with filters * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Cancel open orders * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOpenOrders) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Order History (WebSocket) Retrieve order history for the authenticated subaccount through the WebSocket connection with comprehensive filtering capabilities, providing access to orders in any status (started, placed, partially filled, filled, cancelled, rejected) with flexible query options. ### Request-Response vs Subscriptions This method provides **on-demand snapshots** of order data with flexible filtering. For **real-time updates** when orders change, use [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates). | Method | Purpose | When to Use | | -------------------------- | ---------------------------- | -------------------------------------------------------------- | | `getOrderHistory` | Comprehensive order querying | Initial load, order history, filtered searches, reconciliation | | Order Updates Subscription | Real-time notifications | Live UI updates, event handling | ### Request #### Request Format ```json { "id": "getorders-1", "method": "post", "params": { "action": "getOrderHistory", "subAccountId": "1867542890123456789", "status": ["filled", "cancelled"], "fromTime": 1704067200000, "toTime": 1704672000000, "limit": 50, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | --------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getOrderHistory"` | | `subAccountId` | string | Yes | Subaccount identifier | | `status` | string\[] | No | Filter by order status: `["started", "placed", "partially filled", "filled", "cancelling", "cancelled", "rejected", "modifying", "modified", "unknown"]` | | `startTime` | integer | No | Start timestamp in milliseconds (inclusive, max 7-day range). Preferred over `fromTime` | | `endTime` | integer | No | End timestamp in milliseconds (inclusive, max 7-day range). Preferred over `toTime` | | `fromTime` | integer | No | Start timestamp in milliseconds (inclusive). Deprecated — use `startTime` instead | | `toTime` | integer | No | End timestamp in milliseconds (inclusive). Deprecated — use `endTime` instead | | `limit` | integer | No | Maximum number of orders to return (default: 50, max: 1000) | | `clientOrderId` | string | No | Filter by client-provided order ID. Must not contain leading or trailing whitespace | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * Maximum time range between `startTime` and `endTime` is **7 days** (604,800,000 milliseconds) * `fromTime` and `toTime` are deprecated aliases for `startTime` and `endTime` #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getOrderHistory", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` #### Filter Combinations The endpoint supports powerful filter combinations: * **All Orders**: Omit `status` to get orders in any status * **Placed Orders Only**: `"status": ["placed"]` * **Historical Orders**: `"status": ["filled", "cancelled"]` with time range * **Recent Activity**: Use `startTime` without `endTime` for orders since a specific time * **Client Order Lookup**: Use `clientOrderId` to retrieve a specific order by its client-assigned ID ### Response Format #### Success Response ```json { "id": "getorders-1", "status": 200, "result": { "status": "success", "response": [ { "order": { "venueId": "1958787130134106112", "clientId": "cli-1958787130134106112" }, "orderId": "1958787130134106112", "symbol": "BTC-USDT", "side": "buy", "type": "LIMIT", "quantity": "10.0", "price": "45000.00", "status": "filled", "timeInForce": "GTC", "createdTime": 1755846234000, "updateTime": 1755846290000, "filledQuantity": "10.0", "filledPrice": "45000.00", "triggeredByLiquidation": false, "reduceOnly": false, "postOnly": false }, { "order": { "venueId": "1958787130134106113", "clientId": "cli-1958787130134106113" }, "orderId": "1958787130134106113", "symbol": "ETH-USDT", "side": "sell", "type": "LIMIT", "quantity": "5.0", "price": "2800.00", "status": "partially filled", "timeInForce": "GTC", "createdTime": 1755846235000, "updateTime": 1755846291000, "filledQuantity": "2.0", "filledPrice": "2800.00", "triggeredByLiquidation": false, "reduceOnly": false, "postOnly": true } ] } } ``` **Order ID Migration Note**: Use `order.venueId` as canonical order ID. `orderId` is deprecated compatibility output. **Note on TP/SL auto-cancel**: When a take-profit or stop-loss order in a linked `normalTpsl` pair fills, the platform automatically cancels the sibling order **without creating a history entry**. The filled leg appears with `status: "Filled"` as expected; the auto-cancelled sibling will not appear in the results. Do not rely on an auto-cancelled sibling row when reconciling linked TP/SL pairs. #### Error Response ```json { "id": "getorders-1", "status": 400, "result": null, "error": { "code": 400, "message": "Failed to get orders" } } ``` ### Implementation Example ```javascript class OrderQuery { constructor(ws) { this.ws = ws; this.pendingRequests = new Map(); } async getOrderHistory(options = {}) { const { status = null, fromTime = null, toTime = null, limit = 50, subAccountId } = options; // Validate 7-day time range if both times provided if (fromTime && toTime) { const maxRange = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds if (toTime - fromTime > maxRange) { throw new Error('Time range cannot exceed 7 days'); } } // Build request (signature required — SubAccountAction EIP-712 type) const request = { id: `getorders-${Date.now()}`, method: "post", params: { action: "getOrderHistory", subAccountId: subAccountId.toString() } }; // Add optional filters if (status) request.params.status = status; if (fromTime) request.params.fromTime = fromTime; if (toTime) request.params.toTime = toTime; if (limit) request.params.limit = limit; // Send and wait for response return this.sendRequest(request); } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); // Handle timeout setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); // 10 second timeout }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result.response); } } } } // Usage Examples // Get All Placed Orders async function getAllPlacedOrders(query, subAccountId) { try { const result = await query.getOrderHistory({ subAccountId, status: ["placed"] }); console.log(`Found ${result.length} placed orders`); return result; } catch (error) { console.error('Failed to get placed orders:', error); throw error; } } // Get Order History with Time Range async function getOrderHistoryWithTimeRange(query, subAccountId, fromTime, toTime) { try { const result = await query.getOrderHistory({ subAccountId, fromTime, toTime, limit: 100 }); console.log(`Found ${result.length} historical orders`); return result; } catch (error) { console.error('Failed to get order history:', error); throw error; } } // Filter by Status async function getOrderHistoryByStatus(query, subAccountId, status) { try { const result = await query.getOrderHistory({ subAccountId, status, limit: 50 }); console.log(`Found ${result.length} ${status} orders`); return result; } catch (error) { console.error(`Failed to get orders with status ${status}:`, error); throw error; } } // Recent Orders async function getRecentOrders(query, subAccountId, hoursBack = 24) { const fromTime = Date.now() - (hoursBack * 60 * 60 * 1000); try { const result = await query.getOrderHistory({ subAccountId, fromTime, limit: 25 }); console.log(`Found ${result.length} recent orders`); return result; } catch (error) { console.error('Failed to get recent orders:', error); throw error; } } ``` ### Integration with Subscriptions Combine request-response queries with subscriptions for complete order management: ```javascript class OrderManager { constructor(ws, query, subAccountId) { this.ws = ws; this.query = query; this.subAccountId = subAccountId; this.orders = new Map(); } async initialize() { // 1. Get initial state (placed orders only) const orders = await this.query.getOrderHistory({ subAccountId: this.subAccountId, status: ["placed"] }); orders.forEach(order => { const key = order.order?.venueId || order.orderId; this.orders.set(key, order); }); // 2. Subscribe to real-time updates this.ws.send(JSON.stringify({ id: "subscribe-orders", method: "subscribe", params: { type: "orders", subAccountId: this.subAccountId } })); } handleOrderUpdate(update) { // Handle subscription events const { eventType } = update.data; const orderKey = update.data.order?.venueId || update.data.orderId; if (eventType === 'cancelled' || eventType === 'filled') { this.orders.delete(orderKey); } else { this.orders.set(orderKey, update.data); } } async reconcile() { // Periodic verification const orders = await this.query.getOrderHistory({ subAccountId: this.subAccountId, status: ["placed"] }); // Sync any discrepancies this.orders.clear(); orders.forEach(order => { const key = order.order?.venueId || order.orderId; this.orders.set(key, order); }); } } ``` ### Code Examples #### Get All Open Orders ```json { "id": "getorders-open", "method": "post", "params": { "action": "getOrderHistory", "status": ["placed"], "subAccountId": "1867542890123456789" } } ``` #### Get Order History ```json { "id": "getorders-history", "method": "post", "params": { "action": "getOrderHistory", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 100, "subAccountId": "1867542890123456789" } } ``` ### Use Cases #### Trading Interface * **Active Orders Management**: Use `"status": ["placed"]` to display orders that can be modified or cancelled * **Order History**: Use time ranges to show historical trading activity #### Analytics and Reporting * **Performance Analysis**: Filter by time range and status to calculate trading metrics * **Audit Trail**: Retrieve complete order history for compliance and reporting * **Risk Management**: Monitor open orders across all symbols #### WebSocket Integration * **Legacy Compatibility**: Replace `getOpenOrders` requests with appropriate filters * **Real-time Updates**: Combine with WebSocket subscriptions for live order status updates ### Advanced Filtering #### Status Combinations ```javascript // Get all non-open orders { status: ["filled", "cancelled", "rejected", "partially filled"] } // Get only successful orders { status: ["filled", "partially filled"] } // Get failed orders { status: ["cancelled", "rejected"] } // Get orders in transition states { status: ["cancelling", "modifying"] } ``` #### Time-based Queries ```javascript // Orders from last 24 hours { startTime: Date.now() - 86400000 } // Orders in specific date range { startTime: 1704067200000, endTime: 1704153600000 } // Orders since specific time (no end) { startTime: 1704067200000 } ``` `fromTime` and `toTime` are deprecated aliases for `startTime` and `endTime` respectively and are still accepted. ### Order Object Structure | Field | Type | Description | | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `order` | object | Canonical order identifier object | | `order.venueId` | string | Canonical venue-generated order ID | | `order.clientId` | string | Optional client-provided order ID | | `orderId` | string | Deprecated legacy order identifier | | `symbol` | string | Trading pair symbol (e.g., "BTC-USDT") | | `side` | string | Order side: `"buy"` or `"sell"` | | `type` | string | Order type: `"LIMIT"`, `"MARKET"`, `"STOP_LOSS"`, `"TAKE_PROFIT"` | | `quantity` | string | Original order quantity | | `price` | string | Order price (empty for market orders) | | `status` | string | Order status (e.g., `"placed"`, `"filled"`, `"cancelled"`) | | `timeInForce` | string | Time in force: `"GTC"`, `"IOC"`, or `"FOK"` | | `createdTime` | integer | Order creation timestamp (Unix milliseconds) | | `updateTime` | integer | Last update timestamp (Unix milliseconds) | | `filledQuantity` | string | Quantity that has been filled | | `filledPrice` | string | Average fill price (VWAP for partial fills) | | `triggerPrice` | string | Trigger price for conditional orders (populated for stop market, stop limit, take profit limit, take profit market; omitted for non-conditional orders) | | `triggerPriceType` | string | Reference price used to evaluate the trigger: `"mark_price"` or `"last_price"` | | `triggeredByLiquidation` | boolean | Whether order was triggered by liquidation | | `reduceOnly` | boolean | Whether order only reduces existing position | | `postOnly` | boolean | Whether order must be maker (no immediate match) | | `triggerPrice` | string | Optional. Trigger price for conditional orders (e.g., stop-loss, take-profit). Omitted when not applicable | | `triggerPriceType` | string | Optional. Trigger price type: `"mark"`, `"last"`, or `"index"`. Omitted when not applicable | | `expiresAt` | integer | Optional. Order expiry timestamp (Unix milliseconds). Present only for orders with an explicit expiry (e.g., `limitGtd` order type) | | `cancelReason` | string | Optional. Human-readable reason the order was cancelled. Omitted when not applicable | #### Multiple Fills Handling When an order has multiple partial fills at different prices: * `filledPrice`: Volume-weighted average price (VWAP) of all fills * `filledQuantity`: Total quantity filled across all partial fills * Example: Order for 10 units fills 3 @ $100 and 5 @ $102: * `filledPrice`: "101.25" (VWAP: (3×100 + 5×102) ÷ 8 = $101.25) * `filledQuantity`: "8" ### Implementation Notes * **Cache Strategically**: Cache order data but validate freshness * **Monitor Changes**: Use real-time updates for order state changes * **Handle Timeouts**: Implement reasonable timeout values * **Error Recovery**: Retry failed requests with backoff ### Validation Rules * Subaccount ID must be valid and accessible * Time range must be valid (`startTime` ≤ `endTime`) * Maximum time range between `startTime` and `endTime` is 7 days (604,800,000 milliseconds) * Limit must be between 1 and 1000 (default: 50) * Status values must be valid: `["started", "placed", "partially filled", "filled", "cancelling", "cancelled", "rejected", "modifying", "modified", "unknown"]` * `clientOrderId` must not contain leading or trailing whitespace ### Next Steps * [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time order change notifications (consolidated subscription) * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Position querying via WebSocket * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Order cancellation via WebSocket * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getOrdersHistory) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Performance History (WebSocket) Retrieve comprehensive performance analytics including historical account value, PnL trends, and trading volume for a subaccount over a specified period through the WebSocket connection. Optimized for charting and performance analysis. ### Request #### Request Format ```json { "id": "performance-history-1", "method": "post", "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "week", "expiresAfter": 1735689600, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getPerformanceHistory"` | | `subAccountId` | string | Yes | Subaccount identifier | | `period` | string | No | Time window: `"day"` (default), `"week"`, `"month"`, `"threeMonth"`, `"ytd"`, `"allTime"` | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * If `period` is omitted, the service returns `day` performance history by default #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getPerformanceHistory", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "performance-history-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "period": "week", "performanceHistory": { "history": [ { "sampledAt": 1736947200000, "accountValue": "10000.50", "pnl": "12.34" }, { "sampledAt": 1736950800000, "accountValue": "10020.10", "pnl": "15.67" }, { "sampledAt": 1736954400000, "accountValue": "10150.25", "pnl": "145.92" } ], "volume": "250000.00" } } } ``` #### Error Response ```json { "id": "performance-history-1", "status": 400, "result": null, "error": { "code": 400, "message": "Failed to retrieve performance history" } } ``` ### Response Fields #### Performance History Object | Field | Type | Description | | ------------------------ | ------- | ----------------------------------------------------------------------------------------- | | `history` | array | Ordered list of performance points for the requested period | | `history[].sampledAt` | integer | Unix timestamp in milliseconds | | `history[].accountValue` | string | Account value at the timestamp | | `history[].pnl` | string | PnL at the timestamp, derived from account equity deltas with external cash flows removed | | `volume` | string | Total volume traded during the requested period | The response also echoes the `subAccountId` and the `period` that was requested. `history[].pnl` is calculated from account equity movement with deposits, withdrawals, and transfers removed from the result. If you previously reconciled this field against realized trade PnL plus funding plus a latest unrealized snapshot, historical values may differ. ### Code Examples #### Get Daily Performance ```json { "id": "daily-performance", "method": "post", "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "day", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Weekly Performance ```json { "id": "weekly-performance", "method": "post", "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "week", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get Monthly Performance ```json { "id": "monthly-performance", "method": "post", "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "month", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Get All-Time Performance ```json { "id": "alltime-performance", "method": "post", "params": { "action": "getPerformanceHistory", "subAccountId": "1867542890123456789", "period": "allTime", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class PerformanceQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getPerformanceHistory(subAccountId, period = "day", expiresAfter = 0) { const signature = await this.signSubAccountAction(subAccountId, "getPerformanceHistory", expiresAfter); const request = { id: `performance-${Date.now()}`, method: "post", params: { action: "getPerformanceHistory", subAccountId, period, expiresAfter, signature } }; return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } } // Usage: Build performance dashboard const query = new PerformanceQuery(ws, signer); // Get weekly performance for charting const weeklyData = await query.getPerformanceHistory("1867542890123456789", "week"); console.log(`Period: ${weeklyData.period}`); console.log(`Volume: $${weeklyData.performanceHistory.volume}`); console.log(`Data points: ${weeklyData.performanceHistory.history.length}`); // Chart the data weeklyData.performanceHistory.history.forEach(point => { console.log(`${new Date(point.sampledAt).toISOString()}: $${point.accountValue} (PnL: $${point.pnl})`); }); ``` ### Use Cases * **Performance Dashboards**: Build comprehensive performance tracking interfaces * **PnL Analysis**: Track profit and loss trends over different time periods * **Trading Analytics**: Analyze trading volume and activity patterns * **Portfolio Reporting**: Generate performance reports for users * **Charting Applications**: Power performance charts and visualizations * **Trend Analysis**: Identify performance patterns and trading behavior ### Period Options | Period | Description | | ------------ | ----------------------- | | `day` | Last 24 hours (default) | | `week` | Last 7 days | | `month` | Last 30 days | | `threeMonth` | Last 90 days | | `ytd` | Year to date | | `allTime` | All available history | ### Data Format Notes * **Timestamps**: Unix timestamp in milliseconds * **Time Series**: Array ordered chronologically * **Numeric Precision**: All numeric values returned as strings to preserve precision * **USDT Denomination**: All monetary values are denominated in USDT ### Common Errors | Error Code | Message | Description | | ---------- | ----------------------------------- | --------------------------------------- | | 400 | Invalid request format | Request payload could not be parsed | | 400 | Failed to retrieve performance data | Internal error retrieving data | | 404 | Subaccount not found | Specified subaccount does not exist | | 401 | Insufficient permissions | Account lacks access to this subaccount | ### Next Steps * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Account information and balances * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Current position details * [Get Trades](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getTrades) - Trade execution details * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getFundingPayments) - Funding payment history * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPerformanceHistory) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Position History (WebSocket) Retrieve closed position history for an authenticated subaccount over WebSocket with symbol, time-range, and pagination filters. ### Request #### Request Format ```json { "id": "get-position-history-1", "method": "post", "params": { "action": "getPositionHistory", "subaccountId": "123456789", "symbol": "BTC-USDT", "startTime": 1769364177000, "endTime": 1769450577000, "limit": 50, "offset": 0, "signature": { "v": 27, "r": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "s": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------- | | `id` | string | Yes | Client-generated request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Request payload container | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getPositionHistory"` | | `subaccountId` | string | Yes | Subaccount ID in request payload. The authenticated selected account is enforced server-side for access control | | `symbol` | string | No | Filter by symbol (for example `"BTC-USDT"`) | | `startTime` | integer | No | Start timestamp in milliseconds (inclusive). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. Maximum range between `startTime` and `endTime` is 30 days. | | `endTime` | integer | No | End timestamp in milliseconds (inclusive). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. Maximum range between `startTime` and `endTime` is 30 days. | | `limit` | integer | No | Number of records to return (default: 100, max: 1000) | | `offset` | integer | No | Number of records to skip (default: 0, max: 10000) | | `signature` | object | Yes | EIP-712 signature for the request | This read endpoint does not require nonce management in its request flow. ### Response Format #### Success Response ```json { "id": "get-position-history-1", "status": 200, "result": { "status": "ok", "response": { "positions": [ { "positionId": "2015847946616049664", "symbol": "BTC-USDT", "side": "long", "entryPrice": "95000", "quantity": "0.001", "closePrice": "96000", "closeReason": "close", "leverage": 10, "realizedPnl": "1", "accumulatedFees": "0.1", "netFunding": "0", "closedAt": 1769450577774, "createdAt": 1769450577000, "tradeId": "123" } ], "hasMore": false }, "requestId": "abcd1234", "request_id": "abcd1234", "timestamp": 1769450577774 } } ``` #### Error Response ```json { "id": "get-position-history-1", "status": 400, "result": null, "error": { "code": "VALIDATION_ERROR", "category": "REQUEST", "message": "Invalid time range", "retryable": false } } ``` ### Position History Object | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------ | | `positionId` | string | Unique position identifier | | `symbol` | string | Trading pair symbol | | `side` | string | Position side (`"long"` or `"short"`) | | `entryPrice` | string | Average entry price | | `quantity` | string | Position size | | `closePrice` | string | Execution price of closure | | `closeReason` | string | Position close reason | | `leverage` | integer | Position leverage at the time of close. Omitted when not set | | `realizedPnl` | string | Realized PnL | | `accumulatedFees` | string | Total fees for the position | | `netFunding` | string | Net funding paid/received | | `closedAt` | integer | Position close timestamp in milliseconds | | `createdAt` | integer | Position creation timestamp in milliseconds | | `tradeId` | string | Associated trade identifier | ### EIP-712 Signature ```javascript const domain = { name: 'Synthetix', version: '1', chainId: 1, verifyingContract: '0x0000000000000000000000000000000000000000' }; const types = { SubAccountAction: [ { name: 'subAccountId', type: 'uint256' }, { name: 'action', type: 'string' }, { name: 'expiresAfter', type: 'uint256' } ] }; const message = { subAccountId: '123456789', action: 'getPositionHistory', expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Error Handling | Error | Description | | -------------------------- | ----------------------------------------------------- | | Invalid request format | Request payload is malformed | | Invalid subaccount | Subaccount does not exist or is not accessible | | Invalid time range | `startTime` is greater than `endTime` | | Time range exceeds maximum | `startTime` and `endTime` are more than 30 days apart | | Offset exceeds maximum | `offset` is greater than 10000 | | Invalid pagination | `limit` or `offset` is outside accepted bounds | `symbol` is a filter field, but this endpoint does not document dedicated symbol-validation errors in its handler path. import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Positions (WebSocket) Retrieve position information for the authenticated subaccount through the WebSocket connection with time-based filtering and pagination capabilities. ### Request-Response vs Subscriptions This method provides **on-demand snapshots** of current positions. For **real-time updates** when positions change, use [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates). | Method | Purpose | When to Use | | ------------------------- | ------------------------------- | ----------------------------------------------------------------- | | `getPositions` | Comprehensive position querying | Initial load, position history, filtered searches, reconciliation | | User Updates Subscription | Real-time notifications | Live PnL updates, liquidation alerts | ### Request #### Request Format ```json { "id": "getpositions-1", "method": "post", "params": { "action": "getPositions", "subAccountId": "1867542890123456789", "status": "open", "symbol": "BTC-USDT", "startTime": 1704067200000, "endTime": 1704153600000, "limit": 50, "offset": 0, "sortBy": "createdAt", "sortOrder": "desc", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------- | | `action` | string | Yes | Must be `"getPositions"` | | `subAccountId` | string | Yes | Subaccount identifier | | `status` | string | No | Filter by position status: `"open"`, `"close"`, or `"update"` | | `symbol` | string | No | Filter by trading symbol (e.g., `"BTC-USDT"`) | | `startTime` | integer | No | Start timestamp in milliseconds (inclusive) | | `endTime` | integer | No | End timestamp in milliseconds (inclusive) | | `fromTime` | integer | No | **Deprecated** — use `startTime` instead | | `toTime` | integer | No | **Deprecated** — use `endTime` instead | | `limit` | integer | No | Maximum number of positions to return (default: 50) | | `offset` | integer | No | Number of positions to skip for pagination (default: 0) | | `sortBy` | string | No | Field to sort results by | | `sortOrder` | string | No | Sort direction: `"asc"` or `"desc"` | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * Returns all positions for the authenticated subaccount * Use time range filters to limit results to specific time periods #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getPositions", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Response Structure | Field | Type | Description | | ----------- | ------- | ------------------------------------------------------------------------------ | | `id` | string | Client-provided request identifier (legacy field, echoed from request) | | `requestId` | string | Client-provided request identifier (current field, echoed from request) | | `status` | integer | HTTP status code (`200` for success, `400`/`500` for errors) | | `result` | array | Array of position objects matching filter criteria (omitted when error occurs) | | `error` | object | Error details (only present when an error occurs) | #### Success Response Examples ##### Multiple Positions (Open and Closed) ```json { "id": "getpositions-1", "requestId": "getpositions-1", "status": 200, "result": [ { "adlBucket": 3, "positionId": "pos_12345", "subAccountId": "1867542890123456789", "symbol": "ETH-USDT", "side": "long", "quantity": "2.5", "entryPrice": "2400.00", "realizedPnl": "50.00", "unrealizedPnl": "125.00", "usedMargin": "612.50", "maintenanceMargin": "306.25", "liquidationPrice": "2160.00", "status": "open", "netFunding": "15.50", "takeProfitOrders": [ { "venueId": "2026771048053084163", "clientId": "cli-tp_001" } ], "takeProfitOrderIds": ["2026771048053084163"], "stopLossOrders": [ { "venueId": "2026771048053084165", "clientId": "cli-sl_002" } ], "stopLossOrderIds": ["2026771048053084165"], "createdAt": 1735680000000, "updatedAt": 1735689900000 }, { "adlBucket": 1, "positionId": "pos_12346", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "short", "quantity": "0.1", "entryPrice": "42000.00", "realizedPnl": "200.00", "unrealizedPnl": "0.00", "usedMargin": "0.00", "maintenanceMargin": "0.00", "liquidationPrice": "0.00", "status": "close", "netFunding": "-8.50", "takeProfitOrders": [], "takeProfitOrderIds": [], "stopLossOrders": [], "stopLossOrderIds": [], "createdAt": 1735685000000, "updatedAt": 1735689800000 } ] } ``` ##### Open Positions Only ```json { "id": "getpositions-2", "requestId": "getpositions-2", "status": 200, "result": [ { "adlBucket": 2, "positionId": "pos_12347", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "side": "long", "quantity": "0.2", "entryPrice": "43500.00", "realizedPnl": "0.00", "unrealizedPnl": "100.00", "usedMargin": "1760.00", "maintenanceMargin": "880.00", "liquidationPrice": "41000.00", "status": "open", "netFunding": "2.25", "takeProfitOrders": [ { "venueId": "2026771048053084166", "clientId": "cli-tp_003" } ], "takeProfitOrderIds": ["2026771048053084166"], "stopLossOrders": [ { "venueId": "2026771048053084167", "clientId": "cli-sl_004" } ], "stopLossOrderIds": ["2026771048053084167"], "createdAt": 1735689000000, "updatedAt": 1735689900000 } ] } ``` ##### Empty Result Set ```json { "id": "getpositions-3", "requestId": "getpositions-3", "status": 200, "result": [] } ``` #### Position Status Types | Status | Description | Characteristics | | -------- | ---------------------- | ---------------------------------------------------------- | | `open` | Active position | Has unrealized PnL, margin requirements, liquidation price | | `close` | Closed position | Only realized PnL, no margin requirements | | `update` | Position being updated | Transitional state during modifications | #### Error Response ```json { "id": "getpositions-1", "requestId": "getpositions-1", "status": 400, "error": { "code": 400, "message": "Invalid time range: startTime must be less than or equal to endTime" } } ``` ### Implementation Example ```javascript async function getPositions(ws, subAccountId, signature, options = {}) { const { status = null, symbol = null, startTime = null, endTime = null, limit = 50, offset = 0, sortBy = null, sortOrder = null, expiresAfter = 0 } = options; const requestId = `getpositions-${Date.now()}`; const params = { action: "getPositions", subAccountId, limit, offset, expiresAfter, signature }; if (status) params.status = status; if (symbol) params.symbol = symbol; if (startTime) params.startTime = startTime; if (endTime) params.endTime = endTime; if (sortBy) params.sortBy = sortBy; if (sortOrder) params.sortOrder = sortOrder; const request = { id: requestId, method: "post", params }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Request timeout")), 10000); const handler = (event) => { const response = JSON.parse(event.data); if (response.id === requestId || response.requestId === requestId) { clearTimeout(timeout); ws.removeEventListener("message", handler); if (response.error) { reject(new Error(response.error.message)); } else { resolve(response.result); // Array of position objects } } }; ws.addEventListener("message", handler); ws.send(JSON.stringify(request)); }); } // Usage: get all open positions const openPositions = await getPositions(ws, subAccountId, signature, { status: "open" }); // Usage: get positions for a specific market const btcPositions = await getPositions(ws, subAccountId, signature, { symbol: "BTC-USDT", startTime: Date.now() - 7 * 24 * 60 * 60 * 1000 // last 7 days }); // Display open positions for (const pos of openPositions) { console.log(`${pos.symbol} ${pos.side}: qty=${pos.quantity}, unrealizedPnl=${pos.unrealizedPnl}`); } ``` ### Implementation Notes * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * Time range filters must specify valid Unix millisecond timestamps * Use pagination (limit/offset) for large result sets ### Position Object Structure ```javascript class PositionMonitor { constructor(query, subAccountId) { this.query = query; this.subAccountId = subAccountId; this.positions = new Map(); this.callbacks = new Map(); this.monitoring = false; this.riskAlerts = new Map(); } async startMonitoring(interval = 1000) { this.monitoring = true; // Initial fetch await this.refreshPositions(); // Set up periodic refresh this.monitoringInterval = setInterval(async () => { if (this.monitoring) { await this.refreshPositions(); } }, interval); } stopMonitoring() { this.monitoring = false; if (this.monitoringInterval) { clearInterval(this.monitoringInterval); } } async refreshPositions() { try { const result = await this.query.getOpenPositions({ subAccountId: this.subAccountId }); const newPositions = new Map(); const changes = []; // Process current positions for (const position of result.positions) { const symbol = position.symbol; const previousPosition = this.positions.get(symbol); newPositions.set(symbol, position); if (!previousPosition) { // New position changes.push({ type: 'opened', symbol, position }); } else if (this.hasPositionChanged(previousPosition, position)) { // Position updated changes.push({ type: 'updated', symbol, previousPosition, position, changes: this.getPositionChanges(previousPosition, position) }); } } // Find closed positions for (const [symbol, position] of this.positions) { if (!newPositions.has(symbol)) { changes.push({ type: 'closed', symbol, position }); } } // Update stored positions this.positions = newPositions; // Check risk alerts this.checkRiskAlerts(result); // Notify callbacks of changes for (const change of changes) { this.notifyCallbacks(change); } } catch (error) { console.error('Position monitoring refresh failed:', error); } } hasPositionChanged(previous, current) { return previous.size !== current.size || previous.unrealizedPnl !== current.unrealizedPnl || previous.markPrice !== current.markPrice || previous.marginRatio !== current.marginRatio; } getPositionChanges(previous, current) { const changes = []; if (previous.size !== current.size) { changes.push({ field: 'size', from: previous.size, to: current.size, delta: parseFloat(current.size) - parseFloat(previous.size) }); } if (previous.unrealizedPnl !== current.unrealizedPnl) { changes.push({ field: 'unrealizedPnl', from: previous.unrealizedPnl, to: current.unrealizedPnl, delta: parseFloat(current.unrealizedPnl) - parseFloat(previous.unrealizedPnl) }); } if (previous.markPrice !== current.markPrice) { changes.push({ field: 'markPrice', from: previous.markPrice, to: current.markPrice, delta: parseFloat(current.markPrice) - parseFloat(previous.markPrice) }); } return changes; } onPositionChange(callback) { const id = Date.now() + Math.random(); this.callbacks.set(id, callback); return () => this.callbacks.delete(id); } notifyCallbacks(change) { for (const callback of this.callbacks.values()) { try { callback(change); } catch (error) { console.error('Position change callback error:', error); } } } // Risk management setRiskAlert(symbol, alertConfig) { this.riskAlerts.set(symbol, alertConfig); } checkRiskAlerts(result) { for (const position of result.positions) { const alert = this.riskAlerts.get(position.symbol); if (!alert) continue; const marginRatio = parseFloat(position.marginRatio); const pnlPct = parseFloat(position.unrealizedPnl) / parseFloat(position.margin); // Check margin ratio alert if (alert.marginRatioThreshold && marginRatio >= alert.marginRatioThreshold) { this.triggerAlert('marginRatio', position, { marginRatio, threshold: alert.marginRatioThreshold }); } // Check PnL percentage alert if (alert.pnlThreshold && pnlPct <= alert.pnlThreshold) { this.triggerAlert('pnlThreshold', position, { pnlPct, threshold: alert.pnlThreshold }); } // Check liquidation distance const markPrice = parseFloat(position.markPrice); const liqPrice = parseFloat(position.liquidationPrice); const liqDistance = Math.abs(markPrice - liqPrice) / markPrice; if (alert.liquidationDistance && liqDistance <= alert.liquidationDistance) { this.triggerAlert('liquidationRisk', position, { liqDistance, threshold: alert.liquidationDistance }); } } } triggerAlert(type, position, data) { const alert = { type, symbol: position.symbol, position, data, timestamp: Date.now() }; console.warn(`🚨 RISK ALERT [${type}] for ${position.symbol}:`, data); // Notify callbacks with alert this.notifyCallbacks({ type: 'alert', alert }); } } // Usage const monitor = new PositionMonitor(query, subAccountId); // Set up risk alerts monitor.setRiskAlert('BTC-USDT', { marginRatioThreshold: 0.8, // 80% margin ratio pnlThreshold: -0.1, // -10% PnL liquidationDistance: 0.05 // 5% from liquidation }); // Listen for position changes const unsubscribe = monitor.onPositionChange((change) => { switch (change.type) { case 'opened': console.log('Position opened:', change.position); break; case 'updated': console.log('Position updated:', change.symbol); for (const fieldChange of change.changes) { console.log(` ${fieldChange.field}: ${fieldChange.from} → ${fieldChange.to}`); } break; case 'closed': console.log('Position closed:', change.symbol); break; case 'alert': console.log('Risk alert:', change.alert); break; } }); await monitor.startMonitoring(); ``` #### Portfolio Analytics ```javascript class PositionAnalytics { constructor(query, subAccountId) { this.query = query; this.subAccountId = subAccountId; } async getPortfolioSummary() { const result = await this.query.getOpenPositions({ subAccountId: this.subAccountId }); const positions = result.positions; const summary = { totalPositions: positions.length, longPositions: 0, shortPositions: 0, totalNotional: 0, totalMargin: 0, totalUnrealizedPnl: 0, avgLeverage: 0, marginUtilization: 0, riskMetrics: { maxSinglePositionRisk: 0, correlationRisk: 'low', // simplified liquidationRisk: 'low' } }; let totalLeverageWeighted = 0; let maxPositionSize = 0; let closeToLiquidation = 0; for (const position of positions) { if (position.side === 'long') { summary.longPositions++; } else { summary.shortPositions++; } const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice); const margin = parseFloat(position.margin); const pnl = parseFloat(position.unrealizedPnl); const leverage = parseFloat(position.leverage); summary.totalNotional += notional; summary.totalMargin += margin; summary.totalUnrealizedPnl += pnl; totalLeverageWeighted += leverage * margin; // Risk metrics if (notional > maxPositionSize) { maxPositionSize = notional; } // Check liquidation risk const markPrice = parseFloat(position.markPrice); const liqPrice = parseFloat(position.liquidationPrice); const liqDistance = Math.abs(markPrice - liqPrice) / markPrice; if (liqDistance < 0.1) { // Within 10% of liquidation closeToLiquidation++; } } if (summary.totalMargin > 0) { summary.avgLeverage = totalLeverageWeighted / summary.totalMargin; summary.marginUtilization = summary.totalMargin / parseFloat(result.summary.accountEquity); } summary.riskMetrics.maxSinglePositionRisk = maxPositionSize / summary.totalNotional; summary.riskMetrics.liquidationRisk = closeToLiquidation > 0 ? 'high' : 'low'; return summary; } async getPositionMetrics() { const result = await this.query.getOpenPositions({ subAccountId: this.subAccountId }); return result.positions.map(position => { const size = parseFloat(position.size); const markPrice = parseFloat(position.markPrice); const entryPrice = parseFloat(position.entryPrice); const margin = parseFloat(position.margin); const unrealizedPnl = parseFloat(position.unrealizedPnl); const liqPrice = parseFloat(position.liquidationPrice); const notional = Math.abs(size) * markPrice; const pnlPct = (unrealizedPnl / margin) * 100; const priceMovePct = ((markPrice - entryPrice) / entryPrice) * 100; const liqDistance = Math.abs(markPrice - liqPrice) / markPrice * 100; return { symbol: position.symbol, side: position.side, size, notional, pnlPct, priceMovePct, liquidationDistance: liqDistance, marginRatio: parseFloat(position.marginRatio) * 100, leverage: parseFloat(position.leverage), riskLevel: this.calculateRiskLevel(position) }; }); } calculateRiskLevel(position) { const marginRatio = parseFloat(position.marginRatio); const markPrice = parseFloat(position.markPrice); const liqPrice = parseFloat(position.liquidationPrice); const liqDistance = Math.abs(markPrice - liqPrice) / markPrice; if (marginRatio > 0.8 || liqDistance < 0.05) { return 'high'; } else if (marginRatio > 0.6 || liqDistance < 0.1) { return 'medium'; } else { return 'low'; } } async getDiversificationMetrics() { const result = await this.query.getOpenPositions({ subAccountId: this.subAccountId }); const assetExposure = new Map(); let totalNotional = 0; for (const position of result.positions) { const asset = position.symbol.split('-')[0]; // Extract base asset const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice); totalNotional += notional; const currentExposure = assetExposure.get(asset) || 0; assetExposure.set(asset, currentExposure + notional); } // Calculate concentration ratios const exposureArray = Array.from(assetExposure.values()).sort((a, b) => b - a); const concentrationRatios = { top1: totalNotional > 0 ? exposureArray[0] / totalNotional : 0, top3: totalNotional > 0 ? exposureArray.slice(0, 3).reduce((sum, val) => sum + val, 0) / totalNotional : 0, herfindahl: totalNotional > 0 ? exposureArray.reduce((sum, val) => sum + Math.pow(val / totalNotional, 2), 0) : 0 }; return { assetExposure: Object.fromEntries(assetExposure), concentrationRatios, diversificationScore: 1 - concentrationRatios.herfindahl // Higher = more diversified }; } } ``` #### Position Risk Manager ```javascript class PositionRiskManager { constructor(query, subAccountId) { this.query = query; this.subAccountId = subAccountId; this.riskLimits = new Map(); } setRiskLimits(limits) { // limits = { // maxPositionSize: 100000, // maxLeverage: 20, // maxMarginUtilization: 0.8, // maxSingleAssetExposure: 0.3 // } this.riskLimits = new Map(Object.entries(limits)); } async checkRiskCompliance() { const result = await this.query.getOpenPositions({ subAccountId: this.subAccountId }); const violations = []; const warnings = []; // Check individual position limits for (const position of result.positions) { const notional = Math.abs(parseFloat(position.size)) * parseFloat(position.markPrice); const leverage = parseFloat(position.leverage); const marginRatio = parseFloat(position.marginRatio); // Max position size const maxSize = this.riskLimits.get('maxPositionSize'); if (maxSize && notional > maxSize) { violations.push({ type: 'positionSize', symbol: position.symbol, value: notional, limit: maxSize, severity: 'high' }); } // Max leverage const maxLev = this.riskLimits.get('maxLeverage'); if (maxLev && leverage > maxLev) { violations.push({ type: 'leverage', symbol: position.symbol, value: leverage, limit: maxLev, severity: 'medium' }); } // Margin ratio warning if (marginRatio > 0.7) { warnings.push({ type: 'marginRatio', symbol: position.symbol, value: marginRatio, threshold: 0.7, severity: marginRatio > 0.8 ? 'high' : 'medium' }); } } // Portfolio-level checks const portfolioMetrics = await this.calculatePortfolioRisk(result); const maxMarginUtil = this.riskLimits.get('maxMarginUtilization'); if (maxMarginUtil && portfolioMetrics.marginUtilization > maxMarginUtil) { violations.push({ type: 'marginUtilization', value: portfolioMetrics.marginUtilization, limit: maxMarginUtil, severity: 'high' }); } return { compliant: violations.length === 0, violations, warnings, portfolioMetrics }; } async calculatePortfolioRisk(result) { const positions = result.positions; const summary = result.summary; const totalNotional = positions.reduce((sum, pos) => { return sum + Math.abs(parseFloat(pos.size)) * parseFloat(pos.markPrice); }, 0); const marginUtilization = parseFloat(summary.totalMargin) / parseFloat(summary.accountEquity); return { totalNotional, marginUtilization, totalUnrealizedPnl: parseFloat(summary.totalUnrealizedPnl), accountEquity: parseFloat(summary.accountEquity), freeCollateral: parseFloat(summary.freeCollateral) }; } async generateRiskReport() { const compliance = await this.checkRiskCompliance(); const analytics = new PositionAnalytics(this.query, this.subAccountId); const diversification = await analytics.getDiversificationMetrics(); const positionMetrics = await analytics.getPositionMetrics(); return { timestamp: Date.now(), compliance, diversification, positions: positionMetrics, riskScore: this.calculateOverallRiskScore(compliance, diversification, positionMetrics) }; } calculateOverallRiskScore(compliance, diversification, positions) { let score = 100; // Start with perfect score // Deduct for violations score -= compliance.violations.length * 15; score -= compliance.warnings.length * 5; // Deduct for concentration score -= (1 - diversification.diversificationScore) * 20; // Deduct for high-risk positions const highRiskPositions = positions.filter(p => p.riskLevel === 'high').length; score -= highRiskPositions * 10; return Math.max(0, Math.min(100, score)); } } ``` ### Position Object Structure #### Position Fields **Important**: All numeric fields in position objects are represented as strings to ensure decimal precision. Timestamps are integers containing Unix milliseconds. | Field | Type | Description | | -------------------- | --------- | ------------------------------------------------------------------------ | | `adlBucket` | integer | ADL priority bucket (1–5). 5 = highest risk, 1 = lowest risk. | | `positionId` | string | Unique position identifier | | `subAccountId` | string | Subaccount identifier | | `symbol` | string | Market symbol (e.g., `"BTC-USDT"`) | | `side` | string | Position side: `"long"` or `"short"` | | `quantity` | string | Position size (always positive) | | `entryPrice` | string | Volume-weighted average entry price | | `realizedPnl` | string | Realized profit/loss | | `unrealizedPnl` | string | Current unrealized profit/loss | | `usedMargin` | string | Margin currently allocated to position | | `maintenanceMargin` | string | Minimum margin for position maintenance | | `liquidationPrice` | string | Price at which position will be liquidated | | `status` | string | Position status: `"open"`, `"close"`, or `"update"` | | `netFunding` | string | Net funding payments received/paid | | `takeProfitOrders` | object\[] | Canonical take-profit order identifiers (`venueId`, optional `clientId`) | | `takeProfitOrderIds` | string\[] | Deprecated take-profit venue identifiers | | `stopLossOrders` | object\[] | Canonical stop-loss order identifiers (`venueId`, optional `clientId`) | | `stopLossOrderIds` | string\[] | Deprecated stop-loss venue identifiers | | `createdAt` | integer | Position creation timestamp (Unix milliseconds) | | `updatedAt` | integer | Last update timestamp (Unix milliseconds) | **Migration Note**: Use `takeProfitOrders[*].venueId` and `stopLossOrders[*].venueId` as canonical references. `takeProfitOrderIds` and `stopLossOrderIds` are deprecated compatibility fields. ### Next Steps * [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time position change notifications * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Order querying via WebSocket * Cancel All Orders - Emergency position management * [REST Unified Endpoint](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getPositions) - Recommended unified approach with historical data support import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Rate Limits (WebSocket) Retrieve current rate limit usage and capacity for the authenticated user through the WebSocket connection. This method provides real-time information about API request consumption. ### Request #### Request Format ```json { "id": "ratelimits-1", "method": "post", "params": { "action": "getRateLimits", "subAccountId": "1867542890123456789", "user": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------- | | `action` | string | Yes | Must be `"getRateLimits"` | | `subAccountId` | string | Yes | Subaccount identifier for authentication | | `user` | string | Yes | Wallet address to check rate limits for | | `expiresAfter` | integer | No | Optional expiration timestamp in milliseconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getRateLimits", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "ratelimits-1", "status": 200, "result": { "status": "success", "response": { "requestsUsed": 45, "requestsCap": 1200 } } } ``` #### Response Fields | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------- | | `requestsUsed` | integer | Total requests used in the current rate limit window | | `requestsCap` | integer | Maximum total requests allowed in the current window | #### Error Response ```json { "id": "ratelimits-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid request body" } } ``` ### Code Examples #### Check Rate Limit Status ```json { "id": "ratelimits-check", "method": "post", "params": { "action": "getRateLimits", "subAccountId": "1867542890123456789", "user": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Validation Rules * Subaccount ID must be valid and accessible * User address must be a valid Ethereum address * Authentication requires valid EIP-712 signature ### Next Steps * [Rate Limits Overview](https://developers.synthetix.io/developer-resources/api/rate-limits) — Comprehensive rate limiting documentation * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getRateLimits) — REST API equivalent import CollateralObject from '../../../../../snippets/collateral-object.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CrossMarginSummaryObject from '../../../../../snippets/cross-margin-summary-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Subaccount (WebSocket) Retrieve complete account information for the authenticated subaccount through the WebSocket connection, including collateral balances, open positions, margin summary, market preferences, and fee rate information. ### Request #### Request Format ```json { "id": "subaccount-1", "method": "post", "params": { "action": "getSubAccount", "subAccountId": "1867542890123456789", "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------- | | `action` | string | Yes | Must be `"getSubAccount"` | | `subAccountId` | string | Yes | Subaccount identifier | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * Returns data for the specified subaccount ### Response Format #### Success Response ```json { "id": "subaccount-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "masterAccountId": null, "subAccountName": "Trading Account 1", "collaterals": [ { "symbol": "USDT", "quantity": "10000.00000000", "withdrawable": "5000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "10000.00000000", "collateralValue": "10000.00000000", "haircutRate": "0", "haircutAdjustment": "0", "price": "1.00000000", "calculatedAt": 0 }, { "symbol": "ETH", "quantity": "2.50000000", "withdrawable": "1.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "4375.00000000", "collateralValue": "5000.00000000", "haircutRate": "0.125", "haircutAdjustment": "625.00000000", "price": "2000.00000000", "calculatedAt": 1735689900000 } ], "crossMarginSummary": { "accountValue": "15750.50", "availableMargin": "8250.00", "totalUnrealizedPnl": "125.50", "maintenanceMargin": "3150.25", "initialMargin": "6300.50", "withdrawable": "9450.00", "adjustedAccountValue": "15750.50", "debt": "0.00" }, "positions": [ { "symbol": "BTC-USDT", "side": "long", "entryPrice": "43500.00", "quantity": "0.25", "pnl": "50.00", "upnl": "125.50", "usedMargin": "2175.00", "maintenanceMargin": "1087.50", "liquidationPrice": "41000.00" } ], "marketPreferences": { "leverages": { "BTC-USDT": 20, "ETH-USDT": 10 } }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "VIP 0" }, "accountLimits": { "maxBorrowCapacity": "100000.00", "maxOrdersPerMarket": 200, "maxSubAccounts": 10, "maxTotalOrders": 1000 } } } ``` #### Response Fields | Field | Type | Description | | -------------------- | -------------- | ------------------------------------------------------------------- | | `subAccountId` | string | Unique subaccount identifier | | `masterAccountId` | string \| null | Parent account ID if this is a delegated subaccount, null otherwise | | `subAccountName` | string | Human-readable subaccount name | | `collaterals` | array | Array of collateral balances held in this subaccount | | `crossMarginSummary` | object | Comprehensive margin and balance information | | `positions` | array | Array of open positions for this subaccount | | `marketPreferences` | object | Market-specific settings (leverage per market) | | `feeRates` | object | Fee rates applicable to this subaccount | | `accountLimits` | object | Account limits for this subaccount | #### Collateral Object #### Cross Margin Summary Object #### Position Fields | Field | Type | Description | | ------------------- | ------ | -------------------------------------------- | | `symbol` | string | Market symbol | | `side` | string | Position side: `"long"` or `"short"` | | `entryPrice` | string | Volume-weighted average entry price | | `quantity` | string | Position size (always positive) | | `pnl` | string | Realized profit/loss | | `upnl` | string | Unrealized profit/loss | | `usedMargin` | string | Margin allocated to this position | | `maintenanceMargin` | string | Minimum margin required to maintain position | | `liquidationPrice` | string | Price at which position will be liquidated | #### Market Preferences | Field | Type | Description | | ----------- | ------ | ------------------------------------------------------------------- | | `leverages` | object | Map of market symbols to leverage values (e.g., `{"BTC-USDT": 20}`) | #### Fee Rates | Field | Type | Description | | -------------- | ------ | --------------------------------------- | | `makerFeeRate` | string | Maker fee rate (e.g., "0.0002" = 0.02%) | | `takerFeeRate` | string | Taker fee rate (e.g., "0.0005" = 0.05%) | | `tierName` | string | Fee tier name (e.g., "VIP 0", "VIP 1") | #### Account Limits | Field | Type | Description | | -------------------- | ------- | ------------------------------------------------------ | | `maxBorrowCapacity` | string | Maximum borrow capacity in USD | | `maxOrdersPerMarket` | integer | Maximum number of open orders per market | | `maxSubAccounts` | integer | Maximum number of subaccounts for the master account | | `maxTotalOrders` | integer | Maximum total number of open orders across all markets | #### Error Response ```json { "id": "subaccount-1", "status": 400, "result": null, "error": { "code": 400, "message": "subaccountId is required" } } ``` ### Implementation Example ```javascript class SubaccountQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getSubAccount(subAccountId, expiresAfter = 0) { // Sign using SubAccountAction type (no nonce required) const signature = await this.signSubAccountAction(subAccountId, "getSubAccount", expiresAfter); const request = { id: `subaccount-${Date.now()}`, method: "post", params: { action: "getSubAccount", subAccountId, expiresAfter, signature } }; return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const signature = await this.signer._signTypedData(domain, types, message); const { v, r, s } = ethers.utils.splitSignature(signature); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result); } } } } // Usage const query = new SubaccountQuery(ws, signer); const subaccount = await query.getSubAccount("1867542890123456789"); console.log('Account Value:', subaccount.crossMarginSummary.accountValue); console.log('Positions:', subaccount.positions.length); ``` ### Implementation Notes * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * Returns data for the specified subaccount ### Next Steps * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Detailed position information * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Order history and status * [SubAccount Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time account updates * [Get Subaccount IDs](https://developers.synthetix.io/developer-resources/api/ws-api/info-websocket/getSubAccountIds) - List all subaccount IDs for a wallet import CollateralObject from '../../../../../snippets/collateral-object.mdx'; import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CrossMarginSummaryObject from '../../../../../snippets/cross-margin-summary-object.mdx'; import DelegateObject from '../../../../../snippets/delegate-object.mdx'; import SubaccountObject from '../../../../../snippets/subaccount-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Subaccounts (WebSocket) Retrieve all subaccounts under the same master account as the authenticated subaccount through the WebSocket connection. Works for both account owners and delegated signers — any subaccount that can authenticate will receive the full list of sibling subaccounts, each including collateral balances, cross margin summary, positions, market preferences, fee rates, and delegated signers. This is a more comprehensive alternative to `getSubAccount` that returns multiple subaccounts with delegation information included. ### Request #### Request Format ```json { "id": "subaccounts-1", "method": "post", "params": { "action": "getSubAccounts", "subAccountId": "1867542890123456789", "expiresAfter": 1704067500000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------- | | `action` | string | Yes | Must be `"getSubAccounts"` | | `subAccountId` | string | Yes | The ID of a subaccount owned by the authenticated wallet | | `expiresAfter` | integer | No | Optional expiration timestamp (milliseconds) | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * Returns all subaccounts associated with the authenticated wallet ### Response Format #### Success Response ```json { "id": "subaccounts-1", "status": 200, "result": { "subAccounts": [ { "subAccountId": "1867542890123456789", "masterAccountId": "1867542890123456788", "subAccountName": "Trading Account 1", "collaterals": [ { "symbol": "USDC", "quantity": "1000.00000000", "withdrawable": "1000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "1000.00", "collateralValue": "1000.00", "haircutRate": "0", "haircutAdjustment": "0.00", "price": "1.0000", "calculatedAt": 0 }, { "symbol": "ETH", "quantity": "0.5000", "withdrawable": "0.5000", "pendingWithdraw": "0.0000", "adjustedCollateralValue": "975.00", "collateralValue": "1000.00", "haircutRate": "0.025", "haircutAdjustment": "25.00", "price": "2000.00", "calculatedAt": 1735689600000 } ], "crossMarginSummary": { "accountValue": "6750.50", "availableMargin": "2415.00", "totalUnrealizedPnl": "75.00", "maintenanceMargin": "1207.50", "initialMargin": "2415.00", "withdrawable": "4335.50", "adjustedAccountValue": "6750.50", "debt": "0.00" }, "positions": [], "marketPreferences": { "leverages": { "BTC-USDT": 20, "ETH-USDT": 10 } }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "Regular User" }, "accountLimits": { "maxBorrowCapacity": "10000.00", "maxOrdersPerMarket": 10, "maxSubAccounts": 10, "maxTotalOrders": 100 }, "delegatedSigners": [ { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "permissions": ["trading"], "expiresAt": null, "addedBy": "0x1234567890abcdef1234567890abcdef12345678" } ] }, { "subAccountId": "1867542890123456790", "masterAccountId": "1867542890123456788", "subAccountName": "Trading Account 2", "collaterals": [ { "symbol": "USDC", "quantity": "5000.00000000", "withdrawable": "5000.00000000", "pendingWithdraw": "0.00000000", "adjustedCollateralValue": "5000.00", "collateralValue": "5000.00", "haircutRate": "0", "haircutAdjustment": "0.00", "price": "1.0000", "calculatedAt": 0 } ], "crossMarginSummary": { "accountValue": "5000.00", "availableMargin": "5000.00", "totalUnrealizedPnl": "0.00", "maintenanceMargin": "0.00", "initialMargin": "0.00", "withdrawable": "5000.00", "adjustedAccountValue": "5000.00", "debt": "0.00" }, "positions": [], "marketPreferences": { "leverages": {} }, "feeRates": { "makerFeeRate": "0.0002", "takerFeeRate": "0.0005", "tierName": "Regular User" }, "accountLimits": { "maxBorrowCapacity": "10000.00", "maxOrdersPerMarket": 10, "maxSubAccounts": 10, "maxTotalOrders": 100 }, "delegatedSigners": [] } ] } } ``` #### Response Fields | Field | Type | Description | | ------------- | ----- | --------------------------- | | `subAccounts` | array | Array of subaccount objects | Each subaccount in the `subAccounts` array includes: | Field | Type | Description | | ------------------ | ----- | ----------------------------------------------------- | | `delegatedSigners` | array | Array of delegated signer objects for this subaccount | #### Collateral Object #### Cross Margin Summary Object #### Delegate Object #### Error Response ```json { "id": "subaccounts-1", "status": 400, "result": null, "error": { "code": 400, "message": "subaccountId is required" } } ``` ### Implementation Example ```javascript class SubaccountsQuery { constructor(ws, signer) { this.ws = ws; this.signer = signer; this.pendingRequests = new Map(); } async getSubAccounts(subAccountId) { const expiresAfter = Date.now() + 300000; // 5 minutes in milliseconds // Sign using SubAccountAction type (no nonce required) const signature = await this.signSubAccountAction(subAccountId, "getSubAccounts", expiresAfter); const request = { id: `subaccounts-${Date.now()}`, method: "post", params: { action: "getSubAccounts", subAccountId, expiresAfter, signature } }; return this.sendRequest(request); } async signSubAccountAction(subAccountId, action, expiresAfter = 0) { const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), action, expiresAfter: BigInt(expiresAfter) }; const sig = await this.signer.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); return { v, r, s }; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result); } } } } // Usage const query = new SubaccountsQuery(ws, signer); const subaccounts = await query.getSubAccounts("1867542890123456789"); subaccounts.subAccounts.forEach(account => { console.log(`Account: ${account.subAccountName}`); console.log(`Value: ${account.crossMarginSummary.accountValue}`); console.log(`Delegates: ${account.delegatedSigners.length}`); }); ``` ### Code Examples #### Get All Subaccounts ```json { "id": "get-subaccounts", "method": "post", "params": { "action": "getSubAccounts", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Notes * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * Returns all subaccounts under the same master account as the authenticated subaccount * Works for both account owners and delegated signers * Includes delegated signers for each subaccount, unlike `getSubAccount` ### Next Steps * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Get a single subaccount * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - Get delegated signers for a single subaccount * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Detailed position information * [SubAccount Updates](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time account updates * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getSubAccounts) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import TradeObject from '../../../../../snippets/trade-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Trades (WebSocket) Retrieve trade execution history (fills) for the authenticated subaccount through the WebSocket connection. Each trade represents a filled order or partial fill that has been executed. ### Request-Response vs Subscriptions This method provides **on-demand snapshots** of trade history. For **real-time updates** when new trades execute, use [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates). | Method | Purpose | When to Use | | -------------------------- | -------------------------- | --------------------------------------------------- | | `getTrades` | Historical trades snapshot | Performance analysis, reconciliation, trade history | | Trade Updates Subscription | Real-time notifications | Live UI updates, trade event handling | ### Request #### Request Format ```json { "id": "trades-1", "method": "post", "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "limit": 100, "offset": 0, "startTime": 1730678400000, "endTime": 1730764800000, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getTrades"` | | `subAccountId` | string | Yes | Subaccount identifier | | `symbol` | string | No | Filter trades by market symbol (e.g., `"BTC-USDT"`) | | `orderId` | string | No | Filter trades by venue order ID (only returns trades for this order) | | `limit` | integer | No | Maximum number of trades to return (default: 100, max: 1000) | | `offset` | integer | No | Number of trades to skip for pagination (default: 0) | | `startTime` | integer | No | Start timestamp in milliseconds (inclusive). Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `endTime` | integer | No | End timestamp in milliseconds (inclusive). If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. | | `expiresAfter` | integer | No | Optional expiration timestamp in Unix seconds (use `0` for no expiration) | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * **30-day lookback cap:** `startTime` cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. If `endTime` is older than 30 days without an explicit `startTime`, the request is rejected. * **Time range cannot exceed 30 days** (2,592,000,000 milliseconds) * Trades are returned in descending order by timestamp (newest first) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getTrades", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "trades-1", "status": 200, "result": { "status": "success", "response": { "trades": [ { "tradeId": "123456789", "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "orderId": "1948058938469519360", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "limit", "price": "50000.50", "quantity": "0.1", "realizedPnl": "125.50", "fee": "5.00", "feeRate": "0.001", "markPrice": "50025.00", "entryPrice": "49500.00", "timestamp": 1704067200500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456790", "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "orderId": "1948058938469519361", "symbol": "ETH-USDT", "side": "sell", "direction": "close long", "orderType": "market", "price": "2999.25", "quantity": "1.5", "realizedPnl": "-75.25", "fee": "4.50", "feeRate": "0.001", "markPrice": "3000.00", "entryPrice": "2950.00", "timestamp": 1704067201000, "maker": true, "reduceOnly": true, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456791", "order": { "venueId": "1948058938469519362", "clientId": "cli-1948058938469519362" }, "orderId": "1948058938469519362", "symbol": "BTC-USDT", "side": "sell", "direction": "open short", "orderType": "stopMarket", "price": "49500.00", "quantity": "0.05", "realizedPnl": "0.00", "fee": "2.48", "feeRate": "0.001", "markPrice": "49450.00", "entryPrice": "50000.00", "timestamp": 1704067201500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": true, "postOnly": false } ], "hasMore": false, "total": 3 } } } ``` #### Empty Result ```json { "id": "trades-1", "status": 200, "result": { "status": "success", "response": { "trades": [], "hasMore": false, "total": 0 } } } ``` #### Error Response ```json { "id": "trades-1", "status": 400, "result": null, "error": { "code": 400, "message": "Invalid time range: startTime must be less than or equal to endTime" } } ``` ### Response Fields | Field | Type | Description | | ------------------------- | ------- | ----------------------------------------------- | | `id` | string | Echoed request identifier | | `status` | integer | HTTP-style status code | | `result.status` | string | `"success"` on success | | `result.response.trades` | array | Array of trade objects | | `result.response.hasMore` | boolean | Whether more trades exist beyond current result | | `result.response.total` | integer | Total number of trades matching the query | ### Trade Object Fields | Field | Type | Description | | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `tradeId` | string | Unique trade identifier | | `order.venueId` | string | Canonical venue-generated order ID | | `order.clientId` | string | Optional client-provided order ID | | `orderId` | string | Deprecated legacy order ID | | `symbol` | string | Trading pair symbol (e.g., `"BTC-USDT"`) | | `side` | string | Trade side: `"buy"` or `"sell"` | | `direction` | string | Position effect (e.g., `"open long"`, `"close long"`, `"open short"`, `"close short"`) | | `orderType` | string | Order type that produced this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `price` | string | Execution price | | `quantity` | string | Filled quantity for this trade | | `realizedPnl` | string | Profit/loss realized from this trade | | `fee` | string | Trading fee charged | | `feeRate` | string | Fee rate applied (maker/taker rate) | | `markPrice` | string | Mark price at time of trade execution | | `entryPrice` | string | Position's average entry price at time of trade | | `timestamp` | integer | Trade execution timestamp (Unix milliseconds) | | `maker` | boolean | Whether this trade provided liquidity (maker) or took liquidity (taker) | | `reduceOnly` | boolean | Whether this trade reduced an existing position | | `triggeredByLiquidation` | boolean | Whether this trade was part of a liquidation | | `postOnly` | boolean | Whether this order was post-only (maker-only) | **Migration Note**: Use `order.venueId` as canonical order reference. `orderId` remains for backward compatibility. ### Code Examples #### Get Recent Trades ```json { "id": "trades-recent", "method": "post", "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "limit": 50, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Symbol ```json { "id": "trades-btc", "method": "post", "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "symbol": "BTC-USDT", "limit": 100, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Time Range ```json { "id": "trades-timerange", "method": "post", "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "startTime": 1730678400000, "endTime": 1730764800000, "limit": 100, "offset": 0, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Order ID ```json { "id": "trades-order", "method": "post", "params": { "action": "getTrades", "subAccountId": "1867542890123456789", "orderId": "1948058938469519360", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Example ```javascript class TradeHistoryQuery { constructor(ws) { this.ws = ws; this.pendingRequests = new Map(); } async getTrades(options = {}) { const { subAccountId, symbol = null, limit = 100, offset = 0, startTime = null, endTime = null, expiresAfter = 0, signature } = options; const request = { id: `trades-${Date.now()}`, method: "post", params: { action: "getTrades", subAccountId: subAccountId.toString(), expiresAfter, signature } }; // Add optional filters if (symbol) request.params.symbol = symbol; if (limit) request.params.limit = limit; if (offset) request.params.offset = offset; if (startTime) request.params.startTime = startTime; if (endTime) request.params.endTime = endTime; return this.sendRequest(request); } async getAllTrades(options) { const allTrades = []; let offset = 0; const limit = options.limit || 100; while (true) { const response = await this.getTrades({ ...options, limit, offset }); if (response.trades) { allTrades.push(...response.trades); } if (!response.hasMore || response.trades.length === 0) { break; } offset += limit; } return allTrades; } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 30000); }); } handleResponse(response) { const pending = this.pendingRequests.get(response.id); if (pending) { this.pendingRequests.delete(response.id); if (response.error) { pending.reject(new Error(response.error.message)); } else { pending.resolve(response.result.response); } } } } // Usage const query = new TradeHistoryQuery(ws); // Get last 7 days of BTC trades const endTime = Date.now(); const startTime = endTime - (7 * 24 * 60 * 60 * 1000); const trades = await query.getTrades({ subAccountId: "1867542890123456789", symbol: "BTC-USDT", startTime, endTime, signature: { v: 28, r: "0x...", s: "0x..." } }); console.log(`Found ${trades.total} trades`); trades.trades.forEach(trade => { console.log(`${trade.side} ${trade.quantity} @ ${trade.price} - PnL: ${trade.realizedPnl}`); }); ``` ### Trade Analysis Example ```javascript function analyzeTradeHistory(trades) { const stats = { totalTrades: trades.length, totalVolume: 0, totalFees: 0, totalPnl: 0, makerTrades: 0, takerTrades: 0, bySymbol: {} }; for (const trade of trades) { const volume = parseFloat(trade.quantity) * parseFloat(trade.price); const fee = parseFloat(trade.fee); const pnl = parseFloat(trade.realizedPnl); stats.totalVolume += volume; stats.totalFees += fee; stats.totalPnl += pnl; if (trade.maker) { stats.makerTrades++; } else { stats.takerTrades++; } // Group by symbol if (!stats.bySymbol[trade.symbol]) { stats.bySymbol[trade.symbol] = { trades: 0, volume: 0, fees: 0, pnl: 0 }; } stats.bySymbol[trade.symbol].trades++; stats.bySymbol[trade.symbol].volume += volume; stats.bySymbol[trade.symbol].fees += fee; stats.bySymbol[trade.symbol].pnl += pnl; } return stats; } // Usage const allTrades = await query.getAllTrades({ subAccountId: "1867542890123456789", startTime: Date.now() - (30 * 24 * 60 * 60 * 1000), endTime: Date.now(), signature: { /* ... */ } }); const stats = analyzeTradeHistory(allTrades); console.log(`Total Volume: $${stats.totalVolume.toFixed(2)}`); console.log(`Total Fees: $${stats.totalFees.toFixed(2)}`); console.log(`Total PnL: $${stats.totalPnl.toFixed(2)}`); console.log(`Maker/Taker: ${stats.makerTrades}/${stats.takerTrades}`); ``` ### Trade Types #### Regular Trades Standard trades from limit or market orders: * Contains standard execution information * May be partial fills (group by `order.venueId` to see all fills for the same order) #### Liquidation Trades Forced trades due to insufficient margin: * Identified by `triggeredByLiquidation: true` * Otherwise contains the same fields as regular trades #### Maker vs Taker * **Maker**: Order added liquidity to the orderbook (`maker: true`) * Typically receives fee rebate or reduced fees * **Taker**: Order removed liquidity from the orderbook (`maker: false`) * Typically pays higher fees ### Validation Rules * Subaccount ID must be valid and accessible * Limit must be between 1 and 1000 (default: 100) * Offset must be non-negative (default: 0) * Symbol must be a valid trading pair (if specified) * `orderId` must be a valid positive integer string matching a venue order ID, and cannot be `0` or exceed the maximum allowed value * Time range cannot exceed 30 days * `startTime` must be less than or equal to `endTime` ### Common Errors | Error | Description | Solution | | --------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Invalid time range` | Start time is after end time, or time range exceeds 30 days | Ensure `startTime` \< `endTime` and range ≤ 30 days | | `Invalid limit` | Limit exceeds maximum | Use limit ≤ 1000 | | `Invalid offset` | Negative offset value | Use offset ≥ 0 | | `Invalid symbol` | Market symbol not recognized | Check available markets | | `Invalid orderId` | orderId is non-numeric, zero, or exceeds the maximum allowed value | Use a positive numeric string matching a valid venue order ID | | `Authentication failed` | Invalid signature | Verify signature generation | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### Next Steps * [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time trade notifications * [Get Open Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOpenOrders) - Query active orders * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Query historical orders * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Query current positions * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTrades) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import TradeObject from '../../../../../snippets/trade-object.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Get Trades For Position (WebSocket) Retrieve trade execution history (fills) for a specific position within a subaccount through the WebSocket connection. Unlike [`getTrades`](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getTrades), which returns all trades for a subaccount, this endpoint filters trades to those associated with a single position identified by `positionId`. ### Request-Response vs Subscriptions This method provides **on-demand snapshots** of trades for a specific position. For **real-time updates** when new trades execute, use [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates). | Method | Purpose | When to Use | | -------------------------- | ------------------------------- | ------------------------------------- | | `getTradesForPosition` | Position-specific trade history | Analyzing fills for a single position | | `getTrades` | All trades for a subaccount | Broad trade history, reconciliation | | Trade Updates Subscription | Real-time notifications | Live UI updates, trade event handling | ### Request #### Request Format ```json { "id": "trades-pos-1", "method": "post", "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42", "limit": 100, "offset": 0, "expiresAfter": 1704067500, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getTradesForPosition"` | | `subAccountId` | string | Yes | Subaccount identifier that owns the position | | `positionId` | string | Yes | Position ID to retrieve trades for (numeric string) | | `limit` | integer | No | Maximum number of trades to return (default: 100, max: 1000) | | `offset` | integer | No | Number of trades to skip for pagination (default: 0) | | `expiresAfter` | integer | No | Optional expiration timestamp in Unix seconds (use `0` for no expiration) | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required, only `expiresAfter` is optional) * `positionId` is required and must be a valid numeric string * Unlike `getTrades`, this endpoint does not support `symbol`, `orderId`, `startTime`, or `endTime` filters #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: "1867542890123456789", action: "getTradesForPosition", expiresAfter: 0 }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "trades-pos-1", "status": 200, "result": { "status": "success", "response": { "trades": [ { "tradeId": "123456789", "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "orderId": "1948058938469519360", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "limit", "price": "50000.50", "quantity": "0.1", "realizedPnl": "0.00", "fee": "5.00", "feeRate": "0.001", "markPrice": "50025.00", "entryPrice": "50000.50", "timestamp": 1704067200500, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false }, { "tradeId": "123456790", "order": { "venueId": "1948058938469519361", "clientId": "cli-1948058938469519361" }, "orderId": "1948058938469519361", "symbol": "BTC-USDT", "side": "buy", "direction": "open long", "orderType": "market", "price": "50100.00", "quantity": "0.05", "realizedPnl": "0.00", "fee": "2.51", "feeRate": "0.001", "markPrice": "50110.00", "entryPrice": "50033.67", "timestamp": 1704067201000, "maker": false, "reduceOnly": false, "triggeredByLiquidation": false, "postOnly": false } ], "hasMore": false } } } ``` #### Empty Result ```json { "id": "trades-pos-1", "status": 200, "result": { "status": "success", "response": { "trades": [], "hasMore": false } } } ``` #### Error Response ```json { "id": "trades-pos-1", "status": 400, "result": null, "error": { "code": 400, "message": "positionId is required" } } ``` ### Response Fields | Field | Type | Description | | ------------------------- | ------- | ------------------------------------------------- | | `id` | string | Echoed request identifier | | `status` | integer | HTTP-style status code | | `result.status` | string | `"success"` on success | | `result.response.trades` | array | Array of trade objects for the specified position | | `result.response.hasMore` | boolean | Whether more trades exist beyond current result | ### Trade Object Fields | Field | Type | Description | | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `tradeId` | string | Unique trade identifier | | `order.venueId` | string | Canonical venue-generated order ID | | `order.clientId` | string | Optional client-provided order ID | | `orderId` | string | Deprecated legacy order ID | | `symbol` | string | Trading pair symbol (e.g., `"BTC-USDT"`) | | `side` | string | Trade side: `"buy"` or `"sell"` | | `direction` | string | Position effect (e.g., `"open long"`, `"close long"`, `"open short"`, `"close short"`) | | `orderType` | string | Order type that produced this trade: `"limit"`, `"market"`, `"stopMarket"`, `"takeProfitMarket"`, `"stopLimit"`, or `"takeProfitLimit"` | | `price` | string | Execution price | | `quantity` | string | Filled quantity for this trade | | `realizedPnl` | string | Profit/loss realized from this trade | | `fee` | string | Trading fee charged | | `feeRate` | string | Fee rate applied (maker/taker rate) | | `markPrice` | string | Mark price at time of trade execution | | `entryPrice` | string | Position's average entry price at time of trade | | `timestamp` | integer | Trade execution timestamp (Unix milliseconds) | | `maker` | boolean | Whether this trade provided liquidity (maker) or took liquidity (taker) | | `reduceOnly` | boolean | Whether this trade reduced an existing position | | `triggeredByLiquidation` | boolean | Whether this trade was part of a liquidation | | `postOnly` | boolean | Whether this order was post-only (maker-only) | **Migration Note**: Use `order.venueId` as canonical order reference. `orderId` remains for backward compatibility. ### Code Examples #### Get All Trades for a Position ```json { "id": "trades-pos-all", "method": "post", "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Paginated Request ```json { "id": "trades-pos-page2", "method": "post", "params": { "action": "getTradesForPosition", "subAccountId": "1867542890123456789", "positionId": "42", "limit": 50, "offset": 50, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Validation Rules * Subaccount ID must be valid and accessible * `positionId` is required and must be a valid numeric string * Limit must be between 0 and 1000 (default: 100) * Offset must be non-negative (default: 0) ### Common Errors | Error | Description | Solution | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `positionId is required` | Missing position ID | Provide `positionId` in request params | | `positionId must be a valid numeric value` | Non-numeric position ID | Use a numeric string for `positionId` | | `subaccountId is required` | Missing subaccount | Include `subAccountId` in request params | | `Invalid limit` | Limit exceeds maximum or is negative | Use limit between 0 and 1000 | | `Invalid offset` | Negative offset value | Use offset >= 0 | | `Authentication failed` | Invalid signature | Verify signature generation | | `500 Internal Server Error` | Trade history contains an unrecognized `direction` value (e.g. `"unknown"`, `""`, or any unexpected string) | Indicates corrupted trade direction data on the backend that requires operator investigation. Previously these cases were silently defaulted to `side: "buy"` in the response. | ### Next Steps * [Get Trades](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getTrades) - Retrieve all trades for a subaccount * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Query current positions * [Get Position History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositionHistory) - Query closed position history * [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) - Real-time trade notifications * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTradesForPosition) - REST API comparison import CommonErrorResponses from "../../../../../snippets/common-error-responses.mdx"; import WsTradeEndpoints from "../../../../../snippets/ws-trade-endpoints.mdx"; ## Get Transfers (WebSocket) Retrieve transfer history for a subaccount through the WebSocket connection. Returns collateral transfers between subaccounts with filtering and pagination support. ### Request #### Request Format ```json { "id": "transfers-1", "method": "post", "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "symbol": "USDT", "limit": 50, "offset": 0, "startTime": 1740220800000, "endTime": 1740307200000, "expiresAfter": 1704153600, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"getTransfers"` | | `subAccountId` | string | Yes | Subaccount identifier | | `symbol` | string | No | Filter by collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `limit` | integer | No | Maximum number of results to return (default: 50, max: 1000) | | `offset` | integer | No | Pagination offset (default: 0) | | `startTime` | number | No | Start timestamp in milliseconds. Cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. | | `endTime` | number | No | End timestamp in milliseconds. If `endTime` is older than 30 days and `startTime` is not provided, the request is rejected. Maximum range between `startTime` and `endTime`: 30 days. | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature using `SubAccountAction` type | **Important Notes:** * This endpoint uses `SubAccountAction` EIP-712 type (no nonce required) * **30-day lookback cap:** `startTime` cannot be more than 30 days in the past. When omitted, defaults to `now − 30 days`. If `endTime` is older than 30 days without an explicit `startTime`, the request is rejected. * Maximum time range is 30 days when using `startTime` and `endTime` * For deposit and withdrawal history, use [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getBalanceUpdates) #### EIP-712 Signature ```javascript const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { SubAccountAction: [ { name: "subAccountId", type: "uint256" }, { name: "action", type: "string" }, { name: "expiresAfter", type: "uint256" }, ], }; const message = { subAccountId: "1867542890123456789", action: "getTransfers", expiresAfter: 0, }; const signature = await signer._signTypedData(domain, types, message); ``` ### Response Format #### Success Response ```json { "id": "transfers-1", "status": 200, "result": { "transfers": [ { "transferId": "12345", "from": "2011391943438766080", "to": "3022502054549877191", "symbol": "USDT", "amount": "100", "transferType": "COLLATERAL_TRANSFER", "status": "success", "timestamp": 1740307200000 }, { "transferId": "12346", "from": "2011391943438766080", "to": "3022502054549877191", "symbol": "WETH", "amount": "0.5", "transferType": "COLLATERAL_TRANSFER", "status": "success", "timestamp": 1740220800000 } ], "total": 2 } } ``` #### Empty Result ```json { "id": "transfers-1", "status": 200, "result": { "transfers": [], "total": 0 } } ``` #### Error Response ```json { "id": "transfers-1", "status": 400, "result": null, "error": { "code": 400, "message": "subAccountId is required" } } ``` ### Response Fields #### Transfer Object | Field | Type | Description | | -------------- | ------ | --------------------------------------------------------------- | | `transferId` | string | Unique transfer identifier (string for JS BigInt compatibility) | | `from` | string | Source subaccount ID | | `to` | string | Destination subaccount ID | | `symbol` | string | Collateral symbol (e.g., `"USDT"`, `"WETH"`) | | `amount` | string | Transfer amount as a decimal string | | `transferType` | string | Transfer type (e.g., `"COLLATERAL_TRANSFER"`) | | `status` | string | Transfer status (e.g., `"success"`, `"pending"`, `"failed"`) | | `errorMessage` | string | Error details when the transfer failed (omitted when empty) | | `timestamp` | number | Unix timestamp in milliseconds when the transfer occurred | ### Code Examples #### Get All Transfers ```json { "id": "all-transfers", "method": "post", "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Symbol ```json { "id": "usdt-transfers", "method": "post", "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "symbol": "USDT", "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Filter by Time Range ```json { "id": "time-range", "method": "post", "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "startTime": 1740220800000, "endTime": 1740307200000, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Paginated Request ```json { "id": "paginated", "method": "post", "params": { "action": "getTransfers", "subAccountId": "1867542890123456789", "limit": 100, "offset": 100, "expiresAfter": 0, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Notes * Results are ordered by timestamp (most recent first) * Default limit is 50 results per request, maximum is 1000 * Maximum time range is 30 days when using `startTime` and `endTime` * Use pagination with `limit` and `offset` for large result sets * Authentication requires signature by account owner or authorized delegate ### Common Errors | Error Code | Message | Description | | ---------- | --------------------------- | ------------------------------------- | | 400 | subAccountId is required | No subaccount ID was provided | | 400 | limit cannot exceed 1000 | Limit parameter exceeds maximum | | 400 | limit must be non-negative | Negative limit value provided | | 400 | offset must be non-negative | Negative offset value provided | | 400 | invalid request parameters | Invalid time range (exceeds 30 days) | | 500 | failed to get transfers | Server error retrieving transfer data | ### Next Steps * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getBalanceUpdates) - Deposit and withdrawal history * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - View current account balances * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/getTransfers) - REST API comparison import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Trade WebSocket The Trade WebSocket provides authenticated, real-time access to trading operations with lower latency than REST endpoints. ### Features * **Low Latency Trading**: Submit orders with minimal delay * **Real-time Updates**: Instant order status notifications * **Persistent Connection**: Single connection for all operations * **Authenticated Connection**: EIP-712 signature-based connection authentication * **Mixed Operations**: Read operations use connection auth, write operations use per-request signatures * **Bidirectional**: Request-response and server-pushed updates ### Connection Flow 1. **Establish Connection**: Connect to WebSocket endpoint 2. **Authenticate**: Send authentication message with signature 3. **Trade**: Send trading commands using `method: "post"` 4. **Subscribe**: Receive real-time updates 5. **Maintain**: Send heartbeats to keep connection alive ### Authentication Patterns The Trade WebSocket uses **two different authentication patterns** depending on the operation: | Operation | Pattern | Details | | ---------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Connection auth** | Stringified typed data | `params.message` contains `JSON.stringify({types, domain, primaryType, message})` with a raw hex `params.signature` | | **Trading operations** | Flat params with signature object | All fields (`subAccountId`, `orders`, `nonce`, etc.) go directly inside `params`, with `signature: {v, r, s}` as a nested object | **Connection authentication** (`method: "auth"`) sends the entire EIP-712 structure as a JSON-stringified string in `params.message`, paired with a raw hex signature string. **Trading operations** (`method: "post"`) use flat parameters with the signature split into `{v, r, s}` components. See [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) for the connection auth flow and [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) for an example of the trading pattern. ### Authentication Authentication is required immediately after connecting to the Trade WebSocket endpoint. See [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) for complete authentication flow and examples. **Quick Summary:** 1. Connect to `wss://papi.synthetix.io/v1/ws/trade` 2. Send authentication message with EIP-712 signature 3. Receive authentication confirmation with `subAccountId` 4. Begin trading operations **Key Points:** * Uses EIP-712 typed data signing * Timestamp must be within ±60 seconds of server time * Authentication establishes which subaccount you're trading with * Connection remains authenticated until disconnected ### Security * Secure private key storage * WSS encrypted connections * Re-authentication on connection drops * Server response validation ### Comparison with REST | Feature | WebSocket | REST | | ----------------- | ----------------- | ------------------------- | | Latency | Lower (\~10-50ms) | Higher (\~50-200ms) | | Connection | Persistent | Per-request | | Real-time Updates | Yes | No (polling required) | | Overhead | Minimal | HTTP headers each request | | Complexity | Higher | Lower | | Best For | Active trading | Occasional trades | ### Available Methods #### Order Management * [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Submit new orders * [Modify Order](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/modifyOrder) - Modify existing order * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Cancel specific orders #### Account Management * [Create Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/createSubaccount) - Create a new subaccount * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Get account details * [Update Leverage](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/updateLeverage) - Adjust market leverage * [Update Subaccount Name](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/updateSubAccountName) - Rename a subaccount * [Withdraw Collateral](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/withdrawCollateral) - Withdraw funds to wallet #### Delegation * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Add a delegated signer * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - List delegated signers * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/removeDelegatedSigner) - Remove a delegated signer #### Data Queries * [Get Open Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOpenOrders) - Query active open orders * [Get Order History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getOrderHistory) - Query order history with filters * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - Query open and closed positions * [Get Position History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositionHistory) - Query closed position history * [Get Trades](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getTrades) - Query trade execution history * [Get Balance Updates](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getBalanceUpdates) - Query deposit/withdrawal history * [Get Funding Payments](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getFundingPayments) - Query funding payment history * [Get Performance History](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPerformanceHistory) - Query performance analytics #### Authentication & Setup * [Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - WebSocket authentication flow * [Timeouts & Heartbeats](https://developers.synthetix.io/developer-resources/api/ws-api/timeouts-heartbeats) - Connection management ### Related Resources * [REST Trade API](https://developers.synthetix.io/developer-resources/api/rest-api/trade) - REST API alternative for trading * [WebSocket Subscriptions](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions) - Real-time data streams import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Modify Order (WebSocket) Modify existing orders by changing their price and/or quantity through the WebSocket connection. This is essentially a cancel-and-replace operation that maintains the order's position in the queue while updating its parameters. ### Action Result Notifications This method executes modifications immediately. Modification events are automatically sent via [SubAccount Updates Subscription](https://developers.synthetix.io/developer-resources/api/ws-api/subscriptions/subAccountUpdates) if subscribed. ### Request #### Request Format ```json { "id": "modify-1", "method": "post", "params": { "action": "modifyOrder", "orderId": "2026771048053084160", "price": "45000.50", "quantity": "2.5", "subAccountId": "1", "nonce": 1703123456789, "expiresAfter": 1703123486, "signature": { "v": 27, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------- | | `action` | string | Yes | Must be `"modifyOrder"` | | `orderId` | string | Yes | Order ID to modify (string for JS BigInt compatibility) | | `price` | string | No\* | New price as decimal string | | `quantity` | string | No\* | New quantity as decimal string | | `triggerPrice` | string | No | New trigger price for trigger orders (empty string if not modified) | | `subAccountId` | string | Yes | Subaccount ID that owns the order | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature components | | `signature.v` | integer | Yes | Recovery ID (`27` or `28`) | | `signature.r` | string | Yes | R component of signature | | `signature.s` | string | Yes | S component of signature | \*At least one of `price`, `quantity`, or `triggerPrice` must be provided for the modification to be valid. ### Response Format #### Success Response ```json { "id": "modify-1", "status": 200, "result": { "order": { "venueId": "2026771048053084160", "clientId": "cli-12345" }, "orderId": "2026771048053084160", "status": "modified", "timestamp": 1704067200000, "price": "45000.50", "quantity": "2.5", "triggerPrice": "46000.00", "cumQty": "0.5", "avgPrice": "45000.00" } } ``` #### Response Fields | Field | Type | Description | | ---------------- | ------- | ---------------------------------------------------- | | `order` | object | Canonical modified order identifier object | | `order.venueId` | string | Canonical venue-generated order ID | | `order.clientId` | string | Optional client-provided order ID | | `orderId` | string | Deprecated legacy modified order ID | | `status` | string | Always `"modified"` for successful modify operations | | `timestamp` | integer | Modification timestamp in milliseconds | | `price` | string | New order price (if modified) | | `quantity` | string | New order quantity (if modified) | | `triggerPrice` | string | New trigger price (if modified, for trigger orders) | | `cumQty` | string | Cumulative filled quantity (if partially filled) | | `avgPrice` | string | Average fill price (if partially filled) | **Notes:** * `order` (canonical), `orderId` (deprecated), `status`, and `timestamp` are always returned * `price`, `quantity`, `triggerPrice`, `cumQty`, and `avgPrice` are optional and only included when relevant **Migration Note**: Use `order.venueId` as canonical. `orderId` is deprecated compatibility output. #### Rejection Response Trading rejections return status 200 with `status: "rejected"` on the result: ```json { "id": "modify-1", "status": 200, "result": { "order": { "venueId": "2026771048053084160", "clientId": "cli-12345" }, "orderId": "2026771048053084160", "status": "rejected", "error": "order 2026771048053084160 not found", "errorCode": "ORDER_NOT_FOUND", "timestamp": 1704067200000 } } ``` #### Error Response Request-level errors return a non-200 status: ```json { "id": "modify-1", "status": 400, "error": { "code": 400, "message": "Order not found or does not belong to this subaccount" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function modifyOrder(ws, signer, subAccountId, orderId, price, quantity) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 30; // EIP-712 signature const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { ModifyOrder: [ { name: "subAccountId", type: "uint256" }, { name: "orderId", type: "uint256" }, { name: "price", type: "string" }, { name: "quantity", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId, orderId, price: price || "", quantity: quantity || "", triggerPrice: "", nonce, expiresAfter }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); // Send request ws.send(JSON.stringify({ id: `modify-${Date.now()}`, method: "post", params: { action: "modifyOrder", orderId, subAccountId, ...(price && { price }), ...(quantity && { quantity }), nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage await modifyOrder(ws, signer, "1867542890123456789", "12345", "45000.50", null); ``` ### Validation Rules #### Request Validation 1. **Action Type**: Must be exactly `"modifyOrder"` 2. **Order ID**: Must be a valid positive integer string 3. **Modification Fields**: At least one of `price`, `quantity`, or `triggerPrice` must be provided 4. **Price Format**: If provided, must be a valid decimal string 5. **Quantity Format**: If provided, must be a valid decimal string 6. **Trigger Price Format**: If provided, must be a valid decimal string 7. **Order Existence**: The order must exist and belong to the specified subaccount 8. **Signature Verification**: EIP-712 signature must be valid for the request parameters #### Business Logic Validation * Order must be in an open/active state * Cannot modify orders that are already filled or cancelled * Price/quantity changes must comply with market rules * Account must have sufficient margin for the modification ### Implementation Notes * **Queue Position**: The operation preserves the order's position in the queue when possible * **Atomic Operation**: The modify operation is atomic - either the entire modification succeeds or fails * **Partial Modification**: If only one field (price, quantity, or triggerPrice) is provided, the other fields retain their current values * **Precision**: All monetary values use string representation to avoid floating-point precision issues * **Authentication**: EIP-712 domain separator must use "Synthetix" for WebSocket endpoints * **Timestamps**: Authentication timestamps must be strictly increasing per subaccount ### Comparison with Cancel+Replace | Feature | Modify Order | Cancel+Replace | | -------------- | ----------------- | -------------------- | | Queue Priority | Can maintain | Loses priority | | Latency | Single round-trip | Two round-trips | | Risk Window | Minimal | Exposed during gap | | Complexity | Simple | Complex coordination | | Failure Modes | Single point | Multiple points | ### Error Handling Common error scenarios: | Error Code | Description | | -------------------------- | ---------------------------------------------------------------- | | `VALIDATION_ERROR` | Request validation failed (invalid format, missing fields, etc.) | | `INVALID_FORMAT` | Request body is not valid JSON | | `INTERNAL_ERROR` | Server-side processing error | | `UNAUTHORIZED` | Authentication failed | | `FORBIDDEN` | Wallet does not own the specified subaccount | | Invalid signature | EIP-712 signature verification failed | | Signature address mismatch | Signature address does not match wallet address | | Nonce already used | Nonce has been used in a previous request (replay protection) | | Request expired | `expiresAfter` timestamp has passed | | Order not found | Order ID does not exist or not owned by user | | Order not modifiable | Order is filled, cancelled, or not a limit order | | Invalid order parameters | New order parameters are invalid | ### Next Steps * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Order cancellation via WebSocket * [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Order placement via WebSocket * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/modifyOrder) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import CommonRequestParams from '../../../../../snippets/common-request-params.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import ExampleSignature from '../../../../../snippets/example-signature.mdx'; import InfoEndpoints from '../../../../../snippets/info-endpoints.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import RateLimitsInfo from '../../../../../snippets/rate-limits-info.mdx'; import SubscriptionEndpoints from '../../../../../snippets/subscription-endpoints.mdx'; import TradeEndpoints from '../../../../../snippets/trade-endpoints.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Place Orders (WebSocket) Place orders through the WebSocket connection for lowest latency trading. ### Request #### Request Format ```json { "id": "place-orders-1", "method": "post", "params": { "action": "placeOrders", "orders": [ { "symbol": "BTC-USDT", "side": "buy", "orderType": "limitGtc", "price": "50000.00", "triggerPrice": "", "quantity": "0.10", "reduceOnly": false, "postOnly": false, "isTriggerMarket": false, "clientOrderId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "closePosition": false } ], "grouping": "na", "source": "direct", "subAccountId": "1867542890123456789", "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"placeOrders"` | | `orders` | array | Yes | Array of order objects (minimum 1 order). Each order uses static fields: `orderType`, `price`, `triggerPrice`, `isTriggerMarket` | | `grouping` | string | No | Order grouping: `"na"`, `"normalTpsl"`, `"positionTpsl"`, `"twap"` | | `source` | string | No | Request source identifier for tracking and rebates (max 100 characters). Use `"direct"` for generic integrations | | `subAccountId` | string | Yes | Subaccount identifier | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Expiration timestamp (Unix seconds) | | `signature` | object | Yes | EIP-712 signature | #### Order Object | Parameter | Type | Required | Description | | ------------------ | ------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `symbol` | string | Yes | Market symbol (e.g., `"BTC-USDT"`) | | `side` | string | Yes | `"buy"` or `"sell"` | | `orderType` | string | Yes | One of: `limitGtc`, `limitIoc`, `limitAlo`, `limitGtd`, `market`, `triggerSl`, `triggerTp`, `twap` | | `price` | string | Yes | Price as string. Use empty string when not applicable (market orders and trigger market) | | `quantity` | string | Yes | Order size as string | | `reduceOnly` | boolean | Yes | Only reduce position | | `postOnly` | boolean | Conditional | Whether order must be maker (no immediate match). Only used for `limitGtc` and `limitGtd` orders. Must be `false` for all other order types | | `triggerPrice` | string | Yes | Trigger price as string. Required for `triggerSl` and `triggerTp`; empty string otherwise | | `isTriggerMarket` | boolean | Yes | Execution type for trigger orders. `true` for market-on-trigger, `false` for limit-on-trigger. Must be `false` for non-trigger types | | `closePosition` | boolean | Yes | Close entire position when triggered (TP/SL orders only). Must be `false` for non-trigger types | | `clientOrderId` | string | No | Client-provided order ID. Must match pattern `^0x[a-fA-F0-9]{32}$` (32 hex chars after 0x) | | `triggerPriceType` | string | Conditional | Price type used for trigger evaluation: `"mark"` or `"last"`. Defaults to `"mark"` when omitted. Applies to `triggerSl` and `triggerTp` orders only | | `expiresAt` | integer | Conditional | Expiration timestamp in Unix seconds. Required for `limitGtd` orders; must be 10 seconds to 24 hours in the future. Not valid for other order types | | `durationSeconds` | integer | Conditional | Total TWAP execution window in seconds. Must be positive. Required for `twap` orders only | **Note on `postOnly`**: The `postOnly` field is a request parameter but is **not included in the EIP-712 signed `Order` type**. It is processed server-side after signature verification. The EIP-712 `Order` type contains: `symbol`, `side`, `quantity`, `orderType`, `price`, `triggerPrice`, `reduceOnly`, `isTriggerMarket`, `clientOrderId`, `closePosition`. ### Order Type Enum and Rules * `limitGtc`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitIoc`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitAlo`: price required; `isTriggerMarket=false`; `triggerPrice=""` * `limitGtd`: price required; `isTriggerMarket=false`; `triggerPrice=""`; `expiresAt` required (10s–24h in the future); `postOnly` accepted * `market`: price=""; `isTriggerMarket=false`; `triggerPrice=""` * `triggerSl`: `triggerPrice` required; `isTriggerMarket` determines execution; if `true` then `price=""`, if `false` then `price` required; optional `triggerPriceType` (`"mark"` or `"last"`, defaults to `"mark"`) * `triggerTp`: same rules as `triggerSl` * `twap`: `triggerPrice` must be empty; `durationSeconds` required (positive integer); optional `price` for limit TWAP execution price; use `grouping: "twap"` #### Examples ##### Limit GTC ```json { "orderType": "limitGtc", "price": "50000", "triggerPrice": "", "isTriggerMarket": false } ``` ##### Limit GTD (Good Till Date) ```json { "orderType": "limitGtd", "price": "50000.00", "triggerPrice": "", "isTriggerMarket": false, "postOnly": false, "closePosition": false, "expiresAt": 1704070800 } ``` ##### Market ```json { "orderType": "market", "price": "", "triggerPrice": "", "isTriggerMarket": false } ``` ##### Trigger SL (Market) ```json { "orderType": "triggerSl", "price": "", "triggerPrice": "45000", "isTriggerMarket": true } ``` ##### Trigger TP (Limit) ```json { "orderType": "triggerTp", "price": "44950", "triggerPrice": "45000", "isTriggerMarket": false } ``` ### Response Format #### Success Response The response contains a `statuses` array with one entry per order. Each status can be one of: `resting`, `filled`, `canceled`, or `error`. ##### Resting Order (Accepted) ```json { "id": "place-orders-1", "status": 200, "result": { "statuses": [ { "resting": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360" } } ] } } ``` ##### Filled Order (Immediately Executed) ```json { "id": "place-orders-1", "status": 200, "result": { "statuses": [ { "filled": { "order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" }, "id": "1948058938469519360", "totalSize": "0.10", "avgPrice": "50000.00" } } ] } } ``` ##### Order Error ```json { "id": "place-orders-1", "status": 200, "result": { "statuses": [ { "error": "Insufficient margin", "errorCode": "INSUFFICIENT_MARGIN", "order": { "venueId": null, "clientId": "cli-1948058938469519360" } } ] } } ``` **Response Fields:** | Field | Type | Description | | ----------------------- | ------- | --------------------------------------------------------------------------- | | `resting.order.venueId` | string | Canonical order ID for accepted orders | | `resting.id` | string | Deprecated order ID for accepted orders | | `resting.expiresAt` | integer | Optional. Unix milliseconds expiry timestamp. Present for `limitGtd` orders | | `filled.order.venueId` | string | Canonical order ID for filled orders | | `filled.id` | string | Deprecated order ID for filled orders | | `filled.totalSize` | string | Total filled quantity | | `filled.avgPrice` | string | Average execution price | | `filled.expiresAt` | integer | Optional. Unix milliseconds expiry timestamp. Present for `limitGtd` orders | | `error` | string | Error message if order failed | | `errorCode` | string | Machine-readable error code if order failed | **Notes:** * Each order in the request gets one status object in the response array * Order statuses are returned in the same order as the request * A 200 response can contain individual order errors in the statuses array * Use `*.order.venueId` as canonical; `id` remains deprecated compatibility output #### Request Error Response ```json { "id": "place-orders-1", "status": 400, "error": { "code": 400, "message": "Failed to process order request" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function placeOrders(ws, signer, subAccountId, orders, grouping = "na") { const nonce = Date.now(); const expiresAfter = nonce + 60000; // EIP-712 signature const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { PlaceOrders: [ { name: "subAccountId", type: "uint256" }, { name: "orders", type: "Order[]" }, { name: "grouping", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ], Order: [ { name: "symbol", type: "string" }, { name: "side", type: "string" }, { name: "orderType", type: "string" }, { name: "price", type: "string" }, { name: "triggerPrice", type: "string" }, { name: "quantity", type: "string" }, { name: "reduceOnly", type: "bool" }, { name: "isTriggerMarket", type: "bool" }, { name: "clientOrderId", type: "string" }, { name: "closePosition", type: "bool" } ] }; const message = { subAccountId: BigInt(subAccountId), orders: Array.isArray(orders) ? orders : [orders], grouping, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); // Send request ws.send(JSON.stringify({ id: `place-orders-${Date.now()}`, method: "post", params: { action: "placeOrders", orders: message.orders, grouping, subAccountId, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Limit order await placeOrders(ws, signer, "1867542890123456789", { symbol: "BTC-USDT", side: "buy", orderType: "limitGtc", price: "50000.00", triggerPrice: "", quantity: "0.10", reduceOnly: false, postOnly: false, isTriggerMarket: false, clientOrderId: "0x" + crypto.randomBytes(16).toString('hex'), closePosition: false }); // Usage: Market order await placeOrders(ws, signer, "1867542890123456789", { symbol: "ETH-USDT", side: "sell", orderType: "market", price: "", triggerPrice: "", quantity: "1.00", reduceOnly: false, postOnly: false, isTriggerMarket: false, clientOrderId: "0x" + crypto.randomBytes(16).toString('hex'), closePosition: false }); ``` ### Implementation Notes * Order parameters must validate locally before API submission * Client order IDs enable request tracking across WebSocket sessions * Authentication timestamps must be strictly increasing per subaccount * EIP-712 domain separator must use "Synthetix" for WebSocket endpoints #### Linked TP/SL sibling cancellation When a take-profit or stop-loss order in a linked `normalTpsl` pair fills, the platform automatically cancels the sibling order **without creating a history entry**: * The filled leg (TP or SL) appears in `getOrderHistory` with `status: "Filled"` as expected. * The auto-cancelled sibling does **not** appear in `getOrderHistory`. Do not rely on an auto-cancelled sibling row appearing in order history when reconciling linked TP/SL pairs. ### Next Steps * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Auth setup * [REST Place Orders](https://developers.synthetix.io/developer-resources/api/rest-api/trade/placeOrders) - REST alternative * [Cancel Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelOrders) - Order cancellation import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Remove All Delegated Signers (WebSocket) Remove all delegated signers from a subaccount through the WebSocket connection, revoking their ability to perform trading actions on behalf of the subaccount. This immediately terminates all trading permissions previously granted to any wallet addresses, providing a fast way to revoke all delegations at once. ### Request #### Request Format ```json { "id": "delegate-remove-all-1", "method": "post", "params": { "action": "removeAllDelegatedSigners", "subAccountId": "1867542890123456789", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------- | | `action` | string | Yes | Must be `"removeAllDelegatedSigners"` | | `subAccountId` | string | Yes | Subaccount identifier | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional request expiration timestamp (Unix seconds) | | `signature` | object | Yes | EIP-712 signature | **Important**: Only the master account owner can remove delegated signers. #### EIP-712 Type Definition ```typescript const RemoveAllDelegatedSignersTypes = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response Format #### Success Response ```json { "id": "delegate-remove-all-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "removedSigners": [ "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A", "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E" ] } } ``` #### Response Fields | Field | Type | Description | | ---------------- | --------- | ---------------------------------------------------------- | | `subAccountId` | string | The subaccount ID that delegated signers were removed from | | `removedSigners` | string\[] | Array of wallet addresses that were removed | #### No Delegations Response ```json { "id": "delegate-remove-all-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "removedSigners": [] } } ``` #### Error Response ```json { "id": "delegate-remove-all-1", "status": 401, "result": null, "error": { "code": 401, "message": "Only master account can remove delegated signers" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function removeAllDelegatedSigners(ws, signer, subAccountId) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes from now (Unix seconds) const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { RemoveAllDelegatedSigners: [ { name: "subAccountId", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); ws.send(JSON.stringify({ id: `delegate-remove-all-${Date.now()}`, method: "post", params: { action: "removeAllDelegatedSigners", subAccountId, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Emergency removal of all delegated signers await removeAllDelegatedSigners( ws, signer, "1867542890123456789" ); ``` ### Code Examples #### Emergency Revocation of All Signers ```json { "id": "emergency-revoke", "method": "post", "params": { "action": "removeAllDelegatedSigners", "subAccountId": "1867542890123456789", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Notes * **Immediate Effect**: All delegations are revoked immediately upon successful execution * **Access Requirements**: Only master account owners can remove delegated signers * **Atomic Operation**: Either all delegations are removed or none are (transaction is atomic) * **Idempotent**: Calling when no delegations exist returns a success response with an empty `removedSigners` array * **Signature Required**: All removal operations require valid EIP-712 signatures * **Recovery**: Removed signers can be re-added individually through the `addDelegatedSigner` endpoint ### Use Cases * **Emergency Lockdown**: Quickly revoke all delegated access in a security event * **Account Reset**: Clear all delegations before setting up new access patterns * **Team Offboarding**: Remove all team member access when restructuring * **Security Rotation**: Revoke all existing delegations before re-adding with new permissions ### Common Errors | Error Code | Message | Description | | ---------- | ------------------------------------------------ | -------------------------------------- | | 401 | Only master account can remove delegated signers | Must be signed by master account owner | | 404 | Subaccount not found | Invalid subaccount ID | ### Next Steps * [Remove Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/removeDelegatedSigner) - Remove a single delegated signer * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - List all delegated signers * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Add new delegation * [Get Subaccounts](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccounts) - View all subaccounts with delegation info * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/removeAllDelegatedSigners) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import DelegationEIP712Types from '../../../../../snippets/delegation-eip712-types.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Remove Delegated Signer (WebSocket) Remove a delegated signer from a subaccount through the WebSocket connection, revoking their ability to perform trading actions on behalf of the subaccount. This immediately terminates the trading permission previously granted to the specified wallet address. ### Request #### Request Format ```json { "id": "delegate-remove-1", "method": "post", "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object | Parameter | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `action` | string | Yes | Must be `"removeDelegatedSigner"` | | `subAccountId` | string | Yes | Subaccount identifier | | `walletAddress` | string | Yes | Ethereum wallet address of the delegated signer to remove (42-character hex format). Note: this request field is `walletAddress`; the EIP-712 signed message uses `delegateAddress` for the same value | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional request expiration timestamp in seconds | | `signature` | object | Yes | EIP-712 signature | **Important**: Only the master account owner can remove delegated signers. #### EIP-712 Type Definition ### Response Format #### Success Response ```json { "id": "delegate-remove-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590" } } ``` #### Cascade Removal Response ```json { "id": "delegate-remove-1", "status": 200, "result": { "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "cascadeRemovedSigners": [ "0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222" ] } } ``` #### Response Fields | Field | Type | Description | | ----------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `subAccountId` | string | The subaccount ID the signer was removed from | | `walletAddress` | string | The removed delegated signer's wallet address | | `cascadeRemovedSigners` | string\[] \| omitted | Wallet addresses of sub-delegates automatically removed because they were created by the removed signer. Omitted when empty | #### Error Response ```json { "id": "delegate-remove-1", "status": 404, "result": null, "error": { "code": 404, "message": "Delegated signer not found" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function removeDelegatedSigner(ws, signer, subAccountId, delegateAddress) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { RemoveDelegatedSigner: [ { name: "subAccountId", type: "uint256" }, { name: "delegateAddress", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), delegateAddress, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); ws.send(JSON.stringify({ id: `delegate-remove-${Date.now()}`, method: "post", params: { action: "removeDelegatedSigner", subAccountId, walletAddress: delegateAddress, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Remove a trading bot's delegation await removeDelegatedSigner( ws, signer, "1867542890123456789", "0x742d35Cc6634C0532925a3b844Bc9e7595f89590" ); ``` ### Code Examples #### Remove Trading Bot Access ```json { "id": "remove-bot", "method": "post", "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590", "nonce": 1735689600000, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Revoke Team Member Access ```json { "id": "remove-team-member", "method": "post", "params": { "action": "removeDelegatedSigner", "subAccountId": "1867542890123456789", "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E", "nonce": 1735689600001, "expiresAfter": 1735689900, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Implementation Notes * **Immediate Effect**: Removal takes effect immediately upon successful execution * **Access Requirements**: Only master account owners can remove delegated signers * **No Self-Removal**: Delegated signers cannot remove themselves (must be done by master account) * **Idempotent**: Attempting to remove a non-existent delegation returns an error * **Active Sessions**: Any active sessions or connections for the removed signer should be terminated * **Pending Operations**: Any pending operations initiated by the removed signer remain valid * **Audit Trail**: All removal actions are logged for security and compliance ### Effect on Active Operations | Operation Type | Effect of Removal | | ------------------- | -------------------------------------------------- | | Open Orders | Remain active (can be cancelled by master account) | | Pending Withdrawals | Continue processing | | Active Sessions | Should be terminated | | API Keys | Should be invalidated | | Subscriptions | Should be cancelled | ### Use Cases * **Security Response**: Immediately revoke access when a delegated signer is compromised * **Team Changes**: Remove access when team members leave or change roles * **Bot Decommission**: Remove trading bot access when no longer needed * **Access Rotation**: Regular removal and re-addition of signers for security * **Emergency Lockdown**: Quick removal of all delegated signers in security events ### Common Errors | Error Code | Message | Description | | ---------- | ------------------------------------------------ | -------------------------------------------- | | 404 | Delegated signer not found | Address is not delegated for this subaccount | | 401 | Only master account can remove delegated signers | Must be signed by master account owner | | 404 | Subaccount not found | Invalid subaccount ID | ### Next Steps * [Add Delegated Signer](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/addDelegatedSigner) - Add new delegation * [Get Delegated Signers](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getDelegatedSigners) - List all delegated signers * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Account details * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/removeDelegatedSigner) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Schedule Cancel (WebSocket) Schedule automatic order cancellation through the WebSocket connection as a safety mechanism (Dead Man's Switch) for WebSocket-based trading systems. ### Request #### Request Format - Schedule Cancellation ```json { "id": "schedule-1", "method": "post", "params": { "action": "scheduleCancel", "timeoutSeconds": 300, "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` #### Request Format - Cancel Schedule ```json { "id": "cancel-schedule-1", "method": "post", "params": { "action": "scheduleCancel", "timeoutSeconds": 0, "nonce": 1704067200000, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | ----------------------- | ------- | -------- | ---------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Flat map containing all request fields | | `params.action` | string | Yes | Must be `"scheduleCancel"` | | `params.timeoutSeconds` | integer | Yes | Timeout in seconds; use `0` to cancel an existing schedule | | `params.nonce` | integer | Yes | Incrementing nonce (Unix ms timestamp as number) | | `params.expiresAfter` | integer | No | Optional expiration timestamp in seconds | | `params.signature` | object | Yes | EIP-712 signature | :::note `subAccountId` is **not** sent in the WebSocket request params. The server derives it from the authenticated connection. However, the client must still include `subAccountId` when constructing the EIP-712 typed data to produce a valid signature. ::: #### Time Requirements * **Minimum timeout**: 5 seconds (`timeoutSeconds >= 5`) * **Maximum timeout**: 86400 seconds (24 hours) * **Cancel schedule**: set `timeoutSeconds: 0` ### Response Format #### Success Response - Scheduled ```json { "id": "schedule-1", "requestId": "schedule-1", "status": 200, "timestamp": 1704067200123, "result": { "status": "success", "response": { "isActive": true, "message": "dead-man-switch armed", "timeoutSeconds": 300, "triggerTime": 1704067500000 } } } ``` #### Success Response - Cancelled ```json { "id": "cancel-schedule-1", "requestId": "cancel-schedule-1", "status": 200, "timestamp": 1704067200456, "result": { "status": "success", "response": { "isActive": false, "message": "dead-man-switch disabled", "timeoutSeconds": 0 } } } ``` #### Error Response ```json { "id": "schedule-1", "requestId": "schedule-1", "status": 400, "timestamp": 1704067200000, "error": { "code": 400, "errorCode": "VALIDATION_ERROR", "category": "REQUEST", "message": "timeoutSeconds must be less than or equal to 86400", "retryable": false } } ``` ### Implementation Example ```javascript class DeadManSwitch { constructor(ws, signer, subAccountId) { this.ws = ws; this.signer = signer; this.subAccountId = subAccountId; this.requestId = 0; this.activeSchedule = null; this.heartbeatInterval = null; } async scheduleCancel(delayMs) { const timeoutSeconds = Math.floor(delayMs / 1000); // Generate nonce const nonce = Date.now(); // Create schedule signature const signature = await this.signScheduleCancel(this.subAccountId, timeoutSeconds, nonce); // Build request — all fields go inside flat params map const request = { id: `schedule-cancel-${++this.requestId}`, method: "post", params: { action: "scheduleCancel", timeoutSeconds, nonce, expiresAfter: Math.floor(nonce / 1000) + 30, // 30 seconds from now (seconds) signature } }; // Send and wait for response const result = await this.sendRequest(request); this.activeSchedule = { timeoutSeconds }; return result; } async cancelSchedule() { const nonce = Date.now(); const signature = await this.signScheduleCancel(this.subAccountId, 0, nonce); const request = { id: `cancel-schedule-${++this.requestId}`, method: "post", params: { action: "scheduleCancel", timeoutSeconds: 0, nonce, expiresAfter: Math.floor(nonce / 1000) + 30, // 30 seconds from now (seconds) signature } }; const result = await this.sendRequest(request); this.activeSchedule = null; return result; } async signScheduleCancel(subAccountId, timeoutSeconds, nonce) { const domain = { name: "Synthetix", version: "1", chainId: environment === 'mainnet' ? 1 : 11155111, // Ethereum Mainnet or Sepolia verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { ScheduleCancel: [ { name: "subAccountId", type: "uint256" }, { name: "timeoutSeconds", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), timeoutSeconds: BigInt(timeoutSeconds), nonce: BigInt(nonce), expiresAfter: BigInt(Math.floor(nonce / 1000) + 30) // 30 seconds from now (seconds) }; const signature = await this.signer._signTypedData(domain, types, message); return ethers.utils.splitSignature(signature); } sendRequest(request) { return new Promise((resolve, reject) => { this.pendingRequests.set(request.id, { resolve, reject }); this.ws.send(JSON.stringify(request)); setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 10000); // 10 second timeout }); } // Automatic heartbeat system startHeartbeat(intervalMs = 30000, emergencyDelayMs = 60000) { this.heartbeatInterval = setInterval(async () => { try { // Extend the dead man's switch await this.scheduleCancel(emergencyDelayMs); console.log(`Heartbeat: Extended dead man's switch by ${emergencyDelayMs}ms`); } catch (error) { console.error('Heartbeat failed - manual intervention required:', error); // Could trigger local emergency procedures here } }, intervalMs); } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } async emergencyShutdown() { console.log('🚨 EMERGENCY SHUTDOWN INITIATED'); try { // Cancel any existing schedule if (this.activeSchedule) { await this.cancelSchedule(); } // Schedule immediate cancellation (minimum 5 seconds) await this.scheduleCancel(5000); console.log('✅ Emergency cancellation scheduled in 5 seconds'); } catch (error) { console.error('🔴 Emergency shutdown failed:', error); throw error; } } } // Usage Examples async function setupTradingSession(deadManSwitch) { try { // Set up dead man's switch for 5 minutes await deadManSwitch.scheduleCancel(5 * 60 * 1000); console.log('Dead man\'s switch activated - orders will cancel in 5 minutes'); // Start automatic heartbeat every 2 minutes deadManSwitch.startHeartbeat(2 * 60 * 1000, 5 * 60 * 1000); console.log('Heartbeat started - automatic extension every 2 minutes'); } catch (error) { console.error('Failed to setup trading session safety:', error); throw error; } } async function cleanupTradingSession(deadManSwitch) { try { // Stop heartbeat deadManSwitch.stopHeartbeat(); // Cancel scheduled cancellation await deadManSwitch.cancelSchedule(); console.log('Trading session safety cleanup complete'); } catch (error) { console.error('Failed to cleanup trading session:', error); // Don't throw - safety is more important than cleanup errors } } ``` ### Error Handling #### Common Errors | `errorCode` | Example message | Description | | ------------------ | ---------------------------------------------------- | -------------------------------- | | `VALIDATION_ERROR` | `timeoutSeconds must be 0 or at least 5` | `timeoutSeconds` below minimum | | `VALIDATION_ERROR` | `timeoutSeconds must be less than or equal to 86400` | `timeoutSeconds` exceeds maximum | | `UNAUTHORIZED` | `nonce is required and must be non-zero` | Missing or zero nonce | | `UNAUTHORIZED` | `expiresAfter must be a future unix timestamp` | Request has expired | ### Next Steps * [Cancel All Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/cancelAllOrders) - Emergency order cancellation * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Connection setup * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/scheduleCancel) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Update Leverage (WebSocket) Adjust leverage settings for trading markets through the WebSocket connection for rapid risk management and position scaling. ### Request #### Request Format ```json { "id": "leverage-1", "method": "post", "params": { "action": "updateLeverage", "symbol": "BTC-USDT", "leverage": "20", "subAccountId": "1867542890123456789", "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains the trading request payload with all fields flattened | #### Params Object Fields | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------ | | `action` | string | Yes | Must be `"updateLeverage"` | | `symbol` | string | Yes | Market symbol (e.g., `"BTC-USDT"`) | | `leverage` | string | Yes | Desired leverage multiplier as string | | `subAccountId` | string | Yes | Subaccount identifier | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature | #### EIP-712 Type Definition ```typescript const UpdateLeverageTypes = { UpdateLeverage: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "leverage", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response Format #### Success Response ```json { "id": "leverage-1", "status": 200, "result": { "symbol": "BTC-USDT", "previousLeverage": "10", "newLeverage": "20" } } ``` **Response Fields:** | Field | Type | Description | | ------------------ | ------ | ------------------------- | | `symbol` | string | Market symbol | | `previousLeverage` | string | Previous leverage setting | | `newLeverage` | string | New leverage setting | #### Error Response ```json { "id": "leverage-1", "status": 400, "result": null, "error": { "code": 400, "message": "Leverage exceeds maximum allowed" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function updateLeverage(ws, signer, subAccountId, symbol, leverage) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 30; // EIP-712 signature using UpdateLeverage type const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { UpdateLeverage: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "leverage", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), symbol, leverage: leverage.toString(), nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); // Send request ws.send(JSON.stringify({ id: `leverage-${Date.now()}`, method: "post", params: { action: "updateLeverage", symbol, leverage: leverage.toString(), subAccountId, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Set leverage to 20x for BTC-USDT await updateLeverage(ws, signer, "1867542890123456789", "BTC-USDT", "20"); // Usage: Set conservative 5x leverage for ETH-USDT await updateLeverage(ws, signer, "1867542890123456789", "ETH-USDT", "5"); ``` ### Important Behavior #### How Leverage Settings Work When you update leverage for a market: * The new leverage setting applies to **all future positions** you open in that market * **Existing positions retain their original leverage** - they are NOT affected * The setting persists until you explicitly change it again **Example:** ``` 1. You have an open BTC position at 10x leverage 2. You call updateLeverage(subAccountId, "BTC-USDT", "25") 3. Your existing position REMAINS at 10x 4. Any NEW BTC positions will open at 25x ``` #### Can You Change Leverage on Existing Positions? **No, you cannot directly change leverage on existing positions.** However: * For **Isolated Positions**: Use `updateIsolatedMargin` (REST API only) to add/remove margin, which changes effective leverage * For **Cross Margin Positions**: Leverage is fixed at position entry ### Error Handling #### Common Errors | Error Code | Message | Description | | ---------- | ------------------- | ------------------------------------------- | | -32000 | Invalid leverage | Leverage exceeds maximum allowed for symbol | | -32001 | Position exists | Cannot change leverage with open positions | | -32002 | Invalid symbol | Market symbol does not exist | | -32003 | Margin insufficient | Not enough margin for new leverage setting | ### Next Steps * [Place Orders](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/placeOrders) - Use updated leverage for trading * Transfer Collateral - Manage margin requirements * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Connection setup * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/updateLeverage) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Update Subaccount Name (WebSocket) Rename an existing trading subaccount through the WebSocket connection. The authenticated subaccount's display name is updated to the new value provided in the request. ### Request #### Request Format ```json { "id": "rename-subaccount-1", "method": "post", "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Scalping Strategy", "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains all parameters for the request | #### Params Object Fields | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | Must be `"updateSubAccountName"` | | `subAccountId` | string | Yes | Subaccount identifier to rename | | `name` | string | Yes | New display name; max 50 characters, alphanumeric and `-`, `.`, `=`, `/`, `+`, `_` only; leading/trailing whitespace is trimmed | | `nonce` | integer | Yes | Incrementing nonce (Unix ms timestamp as number) | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature | #### EIP-712 Type Definition ```typescript const UpdateSubAccountNameTypes = { UpdateSubAccountName: [ { name: "subAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response Format #### Success Response ```json { "id": "rename-subaccount-1", "requestId": "rename-subaccount-1", "status": 200, "timestamp": 1704067200123, "result": { "status": "success", "response": { "subAccountId": "1867542890123456789", "name": "Scalping Strategy" } } } ``` **Response Fields:** | Field | Type | Description | | ------------------------------ | ------- | ------------------------------------------------------- | | `id` | string | Echo of the client-provided request identifier | | `requestId` | string | Same value as `id` (included for forward compatibility) | | `status` | integer | HTTP status code | | `timestamp` | integer | Server timestamp in milliseconds (Unix epoch) | | `result.status` | string | `"success"` on successful requests | | `result.response.subAccountId` | string | Subaccount identifier that was renamed | | `result.response.name` | string | The new display name that was applied | #### Error Response ```json { "id": "rename-subaccount-1", "requestId": "rename-subaccount-1", "status": 400, "timestamp": 1704067200456, "error": { "code": 400, "errorCode": "VALIDATION_ERROR", "category": "REQUEST", "message": "subAccountId is required", "retryable": false } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function updateSubAccountName(ws, signer, subAccountId, name) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 30; const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { UpdateSubAccountName: [ { name: "subAccountId", type: "uint256" }, { name: "name", type: "string" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), name, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); ws.send(JSON.stringify({ id: `rename-subaccount-${Date.now()}`, method: "post", params: { action: "updateSubAccountName", subAccountId, name, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Rename a subaccount await updateSubAccountName(ws, signer, "1867542890123456789", "Grid Trading Bot"); ``` ### Code Examples #### Rename Subaccount ```json { "id": "rename-1", "method": "post", "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Main Trading", "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` #### Rename to Strategy-Based Name ```json { "id": "rename-2", "method": "post", "params": { "action": "updateSubAccountName", "subAccountId": "1867542890123456789", "name": "Grid Trading Bot", "nonce": 1704067200001, "expiresAfter": 1704067301, "signature": { "v": 28, "r": "0x...", "s": "0x..." } } } ``` ### Error Handling #### Common Errors | Error Code | Message | Description | | ---------- | ------------------------ | ----------------------------------------- | | 400 | subAccountId is required | No subaccount was specified | | 400 | Invalid request body | Request payload could not be parsed | | 400 | Name already in use | Another subaccount already uses this name | | 404 | Subaccount not found | The specified subaccount does not exist | ### Next Steps * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Verify the name change * [Create Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/createSubaccount) - Create a new subaccount * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/updateSubAccountName) - REST API comparison import CommonErrorResponses from '../../../../../snippets/common-error-responses.mdx'; import EIP712Signing from '../../../../../snippets/eip712-signing.mdx'; import NonceManagement from '../../../../../snippets/nonce-management.mdx'; import WsTradeEndpoints from '../../../../../snippets/ws-trade-endpoints.mdx'; ## Withdraw Collateral (WebSocket) Withdraw collateral from your Synthetix trading account back to your wallet through the WebSocket connection. Withdrawals are subject to approval, margin requirements, and risk checks. ### Request #### Request Format ```json { "id": "withdraw-1", "method": "post", "params": { "action": "withdrawCollateral", "subAccountId": "1867542890123456789", "symbol": "USDC", "amount": "1000.0", "destination": "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2", "nonce": 1704067200000, "expiresAfter": 1704067300, "signature": { "v": 28, "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } } } ``` ### Parameters #### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------------------------------- | | `id` | string | Yes | Client-generated unique request identifier | | `method` | string | Yes | Must be `"post"` | | `params` | object | Yes | Contains the withdrawal request payload with all fields flattened | #### Params Object Fields | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------- | | `action` | string | Yes | Must be `"withdrawCollateral"` | | `subAccountId` | string | Yes | Subaccount identifier | | `symbol` | string | Yes | Collateral asset symbol to withdraw (e.g., `"USDC"`, `"ETH"`) | | `amount` | string | Yes | Amount to withdraw as decimal string (e.g., `"1000.0"`) | | `destination` | string | Yes | Destination wallet address (0x-prefixed Ethereum address) | | `nonce` | integer | Yes | Positive integer, incrementing nonce | | `expiresAfter` | integer | No | Optional expiration timestamp in seconds (0 = no expiration) | | `signature` | object | Yes | EIP-712 signature | **Important**: Withdrawals must be signed by the master account owner. Delegate addresses cannot initiate withdrawals. #### EIP-712 Type Definition ```typescript const WithdrawCollateralTypes = { WithdrawCollateral: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "amount", type: "string" }, { name: "destination", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] } ``` ### Response Format #### Success Response ```json { "id": "withdraw-1", "status": 200, "result": { "requestId": "5ccf215d37e3ae6d", "symbol": "USDC", "amount": "1000.0", "destination": "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2", "timestamp": 0, "withdrawTime": 0 } } ``` **Response Fields:** | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------------------------------ | | `requestId` | string | Echoes the client `request_id` from the response envelope for end-to-end correlation | | `symbol` | string | Asset symbol being withdrawn | | `amount` | string | Amount being withdrawn | | `destination` | string | Destination wallet address | | `timestamp` | integer | Deprecated — always returns `0` (pending removal) | | `withdrawTime` | integer | Withdrawal processing timestamp in milliseconds — currently returns `0` | #### Error Response ```json { "id": "withdraw-1", "status": 400, "result": null, "error": { "code": 400, "message": "Insufficient withdrawable balance" } } ``` ### Implementation Example ```javascript import { ethers } from 'ethers'; async function withdrawCollateral(ws, signer, subAccountId, symbol, amount, destination) { const nonce = Date.now(); const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes // EIP-712 signature using WithdrawCollateral type const domain = { name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000" }; const types = { WithdrawCollateral: [ { name: "subAccountId", type: "uint256" }, { name: "symbol", type: "string" }, { name: "amount", type: "string" }, { name: "destination", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiresAfter", type: "uint256" } ] }; const message = { subAccountId: BigInt(subAccountId), symbol, amount, destination, nonce: BigInt(nonce), expiresAfter: BigInt(expiresAfter) }; const sig = await signer.signTypedData(domain, types, message); const signature = ethers.Signature.from(sig); // Send request ws.send(JSON.stringify({ id: `withdraw-${Date.now()}`, method: "post", params: { action: "withdrawCollateral", subAccountId, symbol, amount, destination, nonce, expiresAfter, signature: { v: signature.v, r: signature.r, s: signature.s } } })); } // Usage: Withdraw 1000 USDC to your wallet await withdrawCollateral( ws, signer, "1867542890123456789", "USDC", "1000.0", "0x742d35Cc6634C0532925a3b8D371d1c62a39b6e2" ); ``` ### Validation Rules * `nonce` must be a positive integer, incrementing and unique per request * `symbol` must be a valid collateral symbol (e.g., `"USDC"`, `"ETH"`) * `amount` must be a positive number as string * `destination` must be a valid Ethereum address (0x-prefixed) * Account must have sufficient withdrawable balance of the specified asset * Withdrawal must not violate margin requirements * Must be signed by master account owner (no delegate support) ### Error Handling #### Common Errors | Error Code | Message | Description | | ---------- | --------------------------------- | ------------------------------------------------ | | 400 | Insufficient withdrawable balance | Account lacks required funds for specified asset | | 400 | Margin requirement violation | Withdrawal would violate margin rules | | 400 | Invalid asset symbol | Asset type not supported as collateral | | 400 | Account health check failed | Withdrawal would make account unhealthy | | 401 | Delegate signature not allowed | Must be signed by account owner | | 400 | Minimum withdrawal amount | Amount below minimum threshold | ### Next Steps * [Deposits](https://developers.synthetix.io/deposits) - How to deposit collateral into your account * [Get Subaccount](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getSubAccount) - Check withdrawable balance * [Get Positions](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/getPositions) - View positions affecting margin * [WebSocket Authentication](https://developers.synthetix.io/developer-resources/api/ws-api/trade-websocket/authentication) - Connection setup * [REST Alternative](https://developers.synthetix.io/developer-resources/api/rest-api/trade/withdrawCollateral) - REST API comparison