Skip to content

Place Orders (WebSocket)

Place orders through the WebSocket connection for lowest latency trading.

Endpoint

ws.send() wss://api.synthetix.io/v1/ws/trade

Request

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

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "post"
paramsobjectYesContains all parameters for the request

Params Object

ParameterTypeRequiredDescription
actionstringYesMust be "placeOrders"
ordersarrayYesArray of order objects (minimum 1 order). Each order uses static fields: orderType, price, triggerPrice, isTriggerMarket
groupingstringNoOrder grouping: "na", "normalTpsl", "positionTpsl", "twap"
sourcestringNoRequest source identifier for tracking and rebates (max 100 characters). Use "direct" for generic integrations
subAccountIdstringYesSubaccount identifier
nonceintegerYesPositive integer, incrementing nonce
expiresAfterintegerNoExpiration timestamp (Unix seconds)
signatureobjectYesEIP-712 signature

Order Object

ParameterTypeRequiredDescription
symbolstringYesMarket symbol (e.g., "BTC-USDT")
sidestringYes"buy" or "sell"
orderTypestringYesOne of: limitGtc, limitIoc, limitAlo, limitGtd, market, triggerSl, triggerTp, twap
pricestringYesPrice as string. Use empty string when not applicable (market orders and trigger market)
quantitystringYesOrder size as string
reduceOnlybooleanYesOnly reduce position
postOnlybooleanConditionalWhether order must be maker (no immediate match). Only used for limitGtc and limitGtd orders. Must be false for all other order types
triggerPricestringYesTrigger price as string. Required for triggerSl and triggerTp; empty string otherwise
isTriggerMarketbooleanYesExecution type for trigger orders. true for market-on-trigger, false for limit-on-trigger. Must be false for non-trigger types
closePositionbooleanYesClose entire position when triggered (TP/SL orders only). Must be false for non-trigger types
clientOrderIdstringNoClient-provided order ID. Must match pattern ^0x[a-fA-F0-9]{32}$ (32 hex chars after 0x)
triggerPriceTypestringConditionalPrice type used for trigger evaluation: "mark" or "last". Defaults to "mark" when omitted. Applies to triggerSl and triggerTp orders only
expiresAtintegerConditionalExpiration timestamp in Unix seconds. Required for limitGtd orders; must be 10 seconds to 24 hours in the future. Not valid for other order types
durationSecondsintegerConditionalTotal TWAP execution window in seconds. Required for twap orders only. Must be between 300 (5 minutes) and 86400 (24 hours)
intervalSecondsintegerNoInterval 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
intervalVarianceBpsintegerNoRandomization of child order timing intervals. Applies to twap orders only. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance)
sizeVarianceBpsintegerNoRandomization 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=""; 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: Time-Weighted Average Price order. triggerPrice=""; expiresAt must be absent; durationSeconds required (300–86400); optional intervalSeconds from the allowed set (10, 30, 60, 120, 180, 300, 600, default 30); optional price sets a limit price, omit for no limit; exactly one order per request; use grouping: "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" }
      }
    ]
  }
}
Response Fields:
FieldTypeDescription
resting.order.venueIdstringCanonical order ID for accepted orders
resting.idstringDeprecated order ID for accepted orders
resting.expiresAtintegerOptional. Unix milliseconds expiry timestamp. Present for limitGtd orders
filled.order.venueIdstringCanonical order ID for filled orders
filled.idstringDeprecated order ID for filled orders
filled.totalSizestringTotal filled quantity
filled.avgPricestringAverage execution price
filled.expiresAtintegerOptional. Unix milliseconds expiry timestamp. Present for limitGtd orders
errorstringError message if order failed
errorCodestringMachine-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

{
  "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:

FieldTypeRequiredDescription
orderTypestringYesMust be "twap"
durationSecondsintegerYesTotal execution window in seconds. Must be between 300 (5 minutes) and 86400 (24 hours)
intervalSecondsintegerNoInterval 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
intervalVarianceBpsintegerNoRandomization of child order timing intervals. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance)
sizeVarianceBpsintegerNoRandomization of child order sizes. Multiples of 100 bps, capped at 2000 bps (20%). Default 0 (no variance)
quantitystringYesTotal quantity across all chunks
pricestringNoOptional limit price; chunks are skipped for any interval where the chunk would fill outside this limit. Omit (empty string) for no limit
sidestringYes"buy" or "sell"
symbolstringYesMarket symbol
reduceOnlybooleanNoStandard reduce-only flag
clientOrderIdstringNoStandard client order ID

Fields that must not be set on a TWAP order (validation fails otherwise):

  • triggerPrice — must be empty
  • expiresAt — 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:

RuleError message
durationSeconds ≥ 300TWAP duration must be at least 5 minutes
durationSeconds ≤ 86400TWAP duration exceeds maximum of 24 hours
intervalSeconds in allowed setTWAP interval invalid, allowed=[10 30 60 120 180 300 600]
Division produces ≥ 2 chunksTWAP duration must produce at least two chunks
quantity > 0TWAP total quantity must be greater than zero
price limit non-negative if setTWAP price limit must be positive
Total notional ≥ $10,000 USDTWAP total order size must be at least $10,000 USD equivalent
Exactly one order in orders arrayrequest error
sizeVarianceBps / intervalVarianceBps multiples of 100request error
sizeVarianceBps / intervalVarianceBps ≤ 2000request 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 price limit 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 twapDetails object 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 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.

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 CodeDescriptionRetryable
UNAUTHORIZEDEIP-712 signature validation failedNo
VALIDATION_ERRORRequest validation failedNo
MISSING_REQUIRED_FIELDRequired field is missingNo
INVALID_FORMATField format is invalidNo
INVALID_VALUEInvalid parameter valueNo
RATE_LIMIT_EXCEEDEDToo many requests in time windowYes
INSUFFICIENT_MARGINNot enough margin for tradeNo
ORDER_NOT_FOUNDOrder does not existNo
OPERATION_TIMEOUTOperation timed outYes

Next Steps