Place Orders (WebSocket)
Place orders through the WebSocket connection for lowest latency trading.
Endpoint
ws.send() wss://api.synthetix.io/v1/ws/tradeRequest
Request Format
{
"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. Required for twap orders only. Must be between 300 (5 minutes) and 86400 (24 hours) |
intervalSeconds | integer | No | Interval between TWAP child chunks in seconds. Applies to twap orders only. Must be one of 10, 30, 60, 120, 180, 300, 600. Omit or set to 0 to use the default of 30 |
intervalVarianceBps | integer | No | Randomization of child order timing intervals. Applies to twap orders only. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance) |
sizeVarianceBps | integer | No | Randomization of child order sizes. Applies to twap orders only. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance) |
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="";expiresAtrequired (10s–24h in the future);postOnlyacceptedmarket: price="";isTriggerMarket=false;triggerPrice=""triggerSl:triggerPricerequired;isTriggerMarketdetermines execution; iftruethenprice="", iffalsethenpricerequired; optionaltriggerPriceType("mark"or"last", defaults to"mark")triggerTp: same rules astriggerSltwap: Time-Weighted Average Price order.triggerPrice="";expiresAtmust be absent;durationSecondsrequired (300–86400); optionalintervalSecondsfrom the allowed set (10,30,60,120,180,300,600, default30); optionalpricesets a limit price, omit for no limit; exactly one order per request; usegrouping: "twap". See TWAP Orders for full details.
Examples
Limit GTC
{
"orderType": "limitGtc",
"price": "50000",
"triggerPrice": "",
"isTriggerMarket": false
}Limit GTD (Good Till Date)
{
"orderType": "limitGtd",
"price": "50000.00",
"triggerPrice": "",
"isTriggerMarket": false,
"postOnly": false,
"closePosition": false,
"expiresAt": 1704070800
}Market
{
"orderType": "market",
"price": "",
"triggerPrice": "",
"isTriggerMarket": false
}Trigger SL (Market)
{
"orderType": "triggerSl",
"price": "",
"triggerPrice": "45000",
"isTriggerMarket": true
}Trigger TP (Limit)
{
"orderType": "triggerTp",
"price": "44950",
"triggerPrice": "45000",
"isTriggerMarket": false
}TWAP
{
"orderType": "twap",
"symbol": "BTC-USDT",
"side": "buy",
"quantity": "1.5",
"price": "",
"triggerPrice": "",
"isTriggerMarket": false,
"reduceOnly": false,
"closePosition": false,
"durationSeconds": 3600,
"intervalSeconds": 60
}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)
{
"id": "place-orders-1",
"status": 200,
"result": {
"statuses": [
{
"resting": {
"order": {
"venueId": "1948058938469519360",
"clientId": "cli-1948058938469519360"
},
"id": "1948058938469519360"
}
}
]
}
}Filled Order (Immediately Executed)
{
"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
{
"id": "place-orders-1",
"status": 200,
"result": {
"statuses": [
{
"error": "Insufficient margin",
"errorCode": "INSUFFICIENT_MARGIN",
"order": { "venueId": null, "clientId": "cli-1948058938469519360" }
}
]
}
}| 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 |
- 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.venueIdas canonical;idremains deprecated compatibility output
Request Error Response
{
"id": "place-orders-1",
"status": 400,
"error": {
"code": 400,
"message": "Failed to process order request"
}
}Implementation Example
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
});TWAP Orders
TWAP (Time-Weighted Average Price) orders split a large order into equal-sized child chunks that are submitted at fixed intervals over a chosen duration. Use TWAPs to reduce market impact when entering or exiting large positions.
A TWAP is placed via the same placeOrders action with grouping: "twap" and orderType: "twap". Exactly one order must be present in the orders array.
TWAP Request
{
"id": "place-twap-1",
"method": "post",
"params": {
"action": "placeOrders",
"subAccountId": "1867542890123456789",
"grouping": "twap",
"orders": [
{
"symbol": "BTC-USDT",
"side": "buy",
"orderType": "twap",
"quantity": "1.5",
"price": "",
"triggerPrice": "",
"isTriggerMarket": false,
"reduceOnly": false,
"closePosition": false,
"durationSeconds": 3600,
"intervalSeconds": 60
}
],
"nonce": 1704067200000,
"expiresAfter": 1704067300,
"signature": {
"v": 28,
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
}TWAP Order Fields
All standard order fields apply. The TWAP-specific fields are:
| Field | Type | Required | Description |
|---|---|---|---|
orderType | string | Yes | Must be "twap" |
durationSeconds | integer | Yes | Total execution window in seconds. Must be between 300 (5 minutes) and 86400 (24 hours) |
intervalSeconds | integer | No | Interval between chunks in seconds. Must be one of 10, 30, 60, 120, 180, 300, 600. Omit or set to 0 to use the default of 30 |
intervalVarianceBps | integer | No | Randomization of child order timing intervals. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance) |
sizeVarianceBps | integer | No | Randomization of child order sizes. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance) |
quantity | string | Yes | Total quantity across all chunks |
price | string | No | Optional limit price; chunks are skipped for any interval where the chunk would fill outside this limit. Omit (empty string) for no limit |
side | string | Yes | "buy" or "sell" |
symbol | string | Yes | Market symbol |
reduceOnly | boolean | No | Standard reduce-only flag |
clientOrderId | string | No | Standard client order ID |
Fields that must not be set on a TWAP order (validation fails otherwise):
triggerPrice— must be emptyexpiresAt— must be absent
Allowed intervalSeconds Values
10, 30, 60, 120, 180, 300, 600.
Any other value returns a request error with TWAP interval invalid, allowed=[10 30 60 120 180 300 600]. When intervalSeconds is 0 or omitted, the server defaults to 30.
TWAP Validation Rules
All rules are enforced server-side and return a request error on failure:
| Rule | Error message |
|---|---|
durationSeconds ≥ 300 | TWAP duration must be at least 5 minutes |
durationSeconds ≤ 86400 | TWAP duration exceeds maximum of 24 hours |
intervalSeconds in allowed set | TWAP interval invalid, allowed=[10 30 60 120 180 300 600] |
| Division produces ≥ 2 chunks | TWAP duration must produce at least two chunks |
quantity > 0 | TWAP total quantity must be greater than zero |
price limit non-negative if set | TWAP price limit must be positive |
| Total notional ≥ $10,000 USD | TWAP total order size must be at least $10,000 USD equivalent |
Exactly one order in orders array | request error |
sizeVarianceBps / intervalVarianceBps multiples of 100 | request error |
sizeVarianceBps / intervalVarianceBps ≤ 2000 | request error |
The notional check uses the current mark price. If the mark price is temporarily unavailable, the request is rejected with mark price not available for TWAP min order size validation.
TWAP child orders are scheduled deterministically using the parent venueOrderId as a seed. When variance fields are set, child order sizes and/or timing intervals vary within the configured bounds.
TWAP Response
On success, the TWAP parent order is accepted and rests in the open orders table immediately. Child-chunk fills arrive asynchronously as the TWAP progresses through each interval. The parent's execution progress can be inspected via getOpenOrders (see the twapDetails object) while the TWAP is active, and via getOrderHistory once it completes or is cancelled. Child-chunk fills and order events flow through the SubAccount Updates subscription like any other order event.
TWAP Signing
TWAP orders use the same PlaceOrders EIP-712 primary type as all other placeOrders requests — there is no separate TWAP action type or signing domain. In the signed payload, set grouping to "twap".
durationSeconds, intervalSeconds, sizeVarianceBps, and intervalVarianceBps are passed in the WebSocket params layer and are not included in the EIP-712 signature. Only the base Order fields and the top-level grouping, subAccountId, nonce, and expiresAfter are signed.
Chunk Execution Behaviour
For reference when building monitoring or progress UIs:
- The TWAP is divided into
floor(durationSeconds / intervalSeconds)equal chunks - Each chunk's quantity is
totalQuantity / numChunks, truncated to market quantity precision. The final chunk absorbs any remainder from truncation - Chunks are submitted as market orders at each interval. If a
pricelimit is set, chunks that would fill outside the limit are skipped for that interval - If the minimum chunk size computes to zero (extreme precision edge case), the TWAP collapses to a single chunk covering the full quantity
- Progress is reported in real time via the
twapDetailsobject on Get Open Orders
Cancelling a TWAP
There is no separate cancel-TWAP endpoint. Use the existing cancelOrders action with the TWAP parent's venue order ID or client order ID, or use cancelAllOrders. Cancelling a TWAP parent automatically cancels any in-flight child chunk orders; the parent transitions to Cancelled status.
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
getOrderHistorywithstatus: "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.
Signing
All trading methods are signed using EIP-712. Each successful trading 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
For detailed information on EIP-712 signing, see EIP-712 Signing.
Nonce Management
The nonce system prevents replay attacks and ensures order uniqueness:
- Use any positive integer as nonce
- Each nonce must be greater than the previous one (incrementing)
Date.now()is a convenient option, not a requirement- If nonce conflicts occur, increment by 1 and retry
:::note SubAccountAction Exception
SubAccountAction endpoints (getPositions, getOpenOrders, getOrderHistory, getTrades, getFundingPayments, getSubAccount, getSubAccounts, getDelegatedSigners, getBalanceUpdates, getWithdrawableAmounts, getFeeRate, getSnaxpotEpochTickets) do not require a nonce. Only the signature and optional expiresAfter parameters are needed.
:::
| Error Code | Description | Retryable |
|---|---|---|
UNAUTHORIZED | EIP-712 signature validation failed | No |
VALIDATION_ERROR | Request validation failed | No |
MISSING_REQUIRED_FIELD | Required field is missing | No |
INVALID_FORMAT | Field format is invalid | No |
INVALID_VALUE | Invalid parameter value | No |
RATE_LIMIT_EXCEEDED | Too many requests in time window | Yes |
INSUFFICIENT_MARGIN | Not enough margin for trade | No |
ORDER_NOT_FOUND | Order does not exist | No |
OPERATION_TIMEOUT | Operation timed out | Yes |
Next Steps
- WebSocket Authentication - Auth setup
- REST Place Orders - REST alternative
- Cancel Orders - Order cancellation