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",
"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" |
subAccountId | string | Yes | Subaccount identifier |
nonce | integer | Yes | Incrementing nonce (Unix ms timestamp as number) |
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, market, triggerSl, triggerTp |
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 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) |
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=""market: price="";isTriggerMarket=false;triggerPrice=""triggerSl:triggerPricerequired;isTriggerMarketdetermines execution; iftruethenprice="", iffalsethenpricerequiredtriggerTp: same rules astriggerSl
Examples
Limit GTC
{
"orderType": "limitGtc",
"price": "50000",
"triggerPrice": "",
"isTriggerMarket": false
}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
}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": {
"id": "1948058938469519360"
}
}
]
}
}Filled Order (Immediately Executed)
{
"id": "place-orders-1",
"status": 200,
"result": {
"statuses": [
{
"filled": {
"id": "1948058938469519360",
"totalSize": "0.10",
"avgPrice": "50000.00"
}
}
]
}
}Order Error
{
"id": "place-orders-1",
"status": 200,
"result": {
"statuses": [
{
"error": "Insufficient margin"
}
]
}
}| Field | Type | Description |
|---|---|---|
resting.id | string | Order ID for accepted orders |
filled.id | string | Order ID for filled orders |
filled.totalSize | string | Total filled quantity |
filled.avgPrice | string | Average execution price |
error | string | Error message 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
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
});Implementation Notes
- Order parameters must validate locally before API submission
- Client order IDs enable request tracking across WebSocket sessions
- Authentication timestamps must be monotonically increasing per subaccount
- EIP-712 domain separator must use "Synthetix" for WebSocket endpoints
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 current timestamp in milliseconds as nonce
- Each nonce must be greater than the previous one
- Recommended: Use
Date.now()or equivalent - If nonce conflicts occur, increment by 1 and retry
:::note SubAccountAction Exception
SubAccountAction endpoints (getPositions, getOpenOrders, getOrdersHistory, getTrades, getFundingPayments, getSubAccount, getDelegatedSigners, getBalanceUpdates) do not require a nonce. Only the signature and optional expiresAfter parameters are needed.
:::
| Error | Description |
|---|---|
| Invalid signature | EIP-712 signature validation failed |
| Invalid market symbol | Market symbol not recognized |
| Nonce already used | Nonce must be greater than previous value |
| Rate limit exceeded | Too many requests in time window |
| Request expired | expiresAfter timestamp has passed |
Next Steps
- WebSocket Authentication - Auth setup
- REST Place Orders - REST alternative
- Cancel Orders - Order cancellation