> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/api-reference/trading/preview-order.md).

# POST /orders/preview

Preview an order

Preview an order.

`operationId`: `previewOrder` · Rate-limit class: `trading`

> **Declared server:** `https://api.testnet.nexus.xyz/v1`. See [Networks](/api-reference/guides/networks.md).

> **Not reachable through CCXT.** Pre-trade simulation. CCXT has no unified preview/dry-run method.

**Authentication:** `agentAuth`, `hmacAuth`.

## Preview an order

> Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.

````json
{"openapi":"3.1.0","info":{"title":"Nexus Exchange API","version":"0.9.101"},"tags":[{"name":"Trading","description":"Order submission and cancellation"}],"servers":[{"url":"https://api.testnet.nexus.xyz/v1","description":"Public testnet (play funds) — the default server and /v1 transport base for unversioned paths."},{"url":"https://api.nexus.xyz/v1","description":"Mainnet — **REAL FUNDS**. Collateral is USDX bridged from Ethereum Mainnet; orders placed here move real money, and there is no faucet. The durable per-network base. Listed after testnet on purpose, so nothing that reaches for \"the first https server\" lands on the real-funds target. Not resolvable yet: DNS/TLS is a separate infra change."},{"url":"http://localhost:9090","description":"Local development — the indexer served directly. Not a public network; whatever your local instance holds."}],"security":[{"hmacAuth":[]},{"agentAuth":[]}],"components":{"securitySchemes":{"hmacAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"HMAC API key authentication. Send three headers:\n- `X-API-Key`: your key ID (e.g. `nx_a1b2c3...`)\n- `X-Timestamp`: current time in unix milliseconds\n- `X-Signature`: `hex(hmac_sha256(secret, timestamp + \"\\n\" + METHOD + \"\\n\" + path + \"\\n\" + query + \"\\n\" + sha256_hex(body)))`\n\nTimestamp must be within 30 seconds of server time.\n\n**The key is network-scoped; the signature is not.** The canonical string above has no network component, so the same signed request is byte-identical on every host. What stops a testnet key from authenticating against mainnet is that the key does not exist in that host's key store — sign only for the host that minted the key, and never replay a signed payload against another network. Presented to the wrong host, a key is refused with the same opaque `401` as an unknown key and must not be retried there. See “API keys are bound to one network” in the API description."},"agentAuth":{"type":"apiKey","in":"header","name":"x-agent","description":"Agent-key request signing. An agent is a secp256k1 keypair the wallet delegates trading to with `POST /agents/register`; the agent then signs each request itself, so the wallet key is never exposed. Send four headers:\n\n- `x-agent`: the agent's 20-byte address, hex, `0x` prefix optional — derived from its public key exactly like a wallet address (`keccak256(uncompressed_pubkey[1..])[12..]`). It must be registered on this host and not expired.\n- `x-timestamp`: the current time in unix milliseconds, as a base-10 integer.\n- `x-nonce`: an unsigned 64-bit integer, base 10. Required on every request; see “Nonces” for when it is enforced.\n- `x-signature`: `0x` followed by the hex of the 65-byte signature described under “Signature”.\n\n**Canonical string.** Six fields joined by LF (`\\n`), with no trailing newline:\n\n```\n{METHOD}\\n{path}\\n{query}\\n{sha256_hex(body)}\\n{x-timestamp}\\n{x-nonce}\n```\n\n- `METHOD` is upper-cased. The field order differs from `hmacAuth`: the method comes first and the timestamp comes after the body hash, so an HMAC canonical-string builder cannot be reused.\n- `path` is the logical path the server authenticates, with no host and no query string — the same rule as `hmacAuth`; see “Base paths and the signed path” in the API description.\n- `query` is the raw query string exactly as sent, without the leading `?`. With no query it is the empty string, and its line stays in the string as an empty line; it is never dropped.\n- `sha256_hex(body)` is the lower-case hex SHA-256 of the exact body bytes sent. A request with no body hashes the empty byte string (`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`).\n- `x-timestamp` and `x-nonce` are the same decimal strings sent in those headers.\n\n**Digest.** `keccak256(utf8(canonical_string))`: raw Keccak-256 of the canonical string, **with no EIP-191 prefix and no EIP-712 structure**. A wallet library's `signMessage` / `personal_sign` adds the `\\x19Ethereum Signed Message:\\n` prefix, which produces a different digest and a signature that recovers to some other address. Sign the 32-byte hash directly (for example `eth_account`'s `unsafe_sign_hash`, or `@noble/curves` `secp256k1.sign(digest, key)`).\n\n**Signature.** Recoverable secp256k1 ECDSA over the digest, sent as 65 bytes `r (32) ‖ s (32) ‖ v (1)` with `v ∈ {27, 28}`. `s` must be low (at most half the curve order, per EIP-2). A high-S signature is refused even though it recovers the same signer, because accepting it would let one authorization be presented as two. The server recovers the signer from the digest and requires it to equal `x-agent`.\n\n**Freshness.** `x-timestamp` must be within ±30 seconds of the server's clock, on reads and writes alike. That window is the only bound on how long a captured read signature can be replayed.\n\n**Nonces.** The nonce is parsed on every request but enforced only on writes:\n\n- On a mutating method (anything but `GET`, `HEAD`, `OPTIONS` and `TRACE`), `x-nonce` must be strictly greater than the highest nonce this host has already accepted on a write from the same agent. The accepted value is persisted before the response is sent. Each agent address has its own nonce space, separate from the wallet-signed nonces of `POST /agents/register` and `POST /withdrawals`.\n- On a read, the nonce is neither checked nor consumed, so the same signed read verifies more than once inside its ±30 s window.\n\nA counter of `max(last + 1, now_ms)` is a simple nonce source: it is unique per signer and stays increasing across restarts.\n\n**Concurrent writes from one agent can be refused as replays.** The rule is about arrival order, not issue order. When two writes from the same agent are in flight together and the one with the higher nonce arrives first, the other is refused, and the refusal is the same opaque `401` as a bad signature. The fix is still being decided (ENG-17010). Until then, keep one write in flight per agent key, or register a separate agent key for each concurrent writer.\n\n**Failures are opaque.** Any failure returns the same `401` body with no reason (see the `Unauthorized` response): an unknown or expired agent, a stale timestamp, a malformed header, a wrong digest, a high-S signature or a replayed nonce all look alike. This contract does not say which check failed or in what order the checks run, and a client must not infer either. If a signer is rejected, check it against the test vector below before anything else.\n\n**What an agent key cannot do.** Agent keys are trade-only. **An agent key cannot withdraw**, and it cannot move funds off the account by any route:\n\n- `POST /api/v1/bridge/withdrawals` refuses an agent-signed request with `403` (code `AGENT_CANNOT_WITHDRAW`). The engine's withdrawal route is refused the same way.\n- `POST /withdrawals` requires the wallet's own EIP-712 signature in the body, which an agent key cannot produce.\n- The ordinary-transfer operations (`ownerSignature`) refuse any request that carries `x-agent`.\n\nAgent keys also cannot manage agent credentials: `GET /agents`, `PATCH /agents/{address}` and `DELETE /agents/{address}` refuse them with `403` (code `AGENT_KEY_FORBIDDEN`). A client that holds only an agent key manages them with the wallet's own signature instead (`walletSignature`). And they cannot mint the legacy `POST /ws-tokens` token, which is HMAC-only; use `POST /ws/token`. None of these operations lists `agentAuth`. Every operation that does list it accepts it.\n\n**Test vector.** The key below is a public test key: never fund it or register it. The `x-nexus-test-vectors` extension on this scheme carries this vector and two more (an empty-body `GET`, and a `DELETE` with a query). Together they cover both values of `v`.\n\n- private key: `0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318` → `x-agent: 0x2c7536e3605d9c16a7a3d7b1898e529396a65c23`\n- request: `post /orders` (the method is lower-case on purpose; the canonical string upper-cases it), no query, body `{\"market_id\":\"BTC-USDX-PERP\",\"side\":\"Buy\",\"order_type\":\"Limit\",\"quantity\":\"0.1\",\"price\":\"50000\"}`, `x-timestamp: 1776033900123`, `x-nonce: 42`\n- canonical string (LF shown as `⏎`): `POST⏎/orders⏎⏎59ddf9017ca0eb5f8802afd1a2d1584b2d93b0260a0fcd62a1b94b3ff92b332c⏎1776033900123⏎42`\n- digest: `0xe27ade6101c417d2c393abfce8f35b6e1551cf0ce5c50dfbe12f8d6f399ffc35`\n- `x-signature: 0xeef09105834062da10d208be38311995d04cbc5e05043b89e7cbe6d4497d11b51f61729639551a8cfb6ea3e4fe0765afeba7bf3b5f5e09f63bc095c89005b61d1c`\n\n**Network scope.** Like every credential here, an agent registration belongs to the host it was registered on. The canonical string has no network component, so the refusal elsewhere comes from the agent being unknown to that host."}},"schemas":{"OrderRequest":{"type":"object","description":"Order placement request. Supports plain `Limit` / `Market` orders and six conditional order types (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`, `TrailingStop`, `TrailingLimit`). Field requirements depend on `order_type`:\n\n- **Limit-family** (`Limit`, `StopLimit`, `TakeProfitLimit`) require a limit `price`.\n- **Triggerable, non-trailing** orders (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`) require a `trigger_price` (the legacy `stop_price` field is accepted as a fallback when `trigger_price` is absent).\n- **`TrailingStop`** is market-only — it fires as a market order — and requires `trailing_offset_bps`. It does not take a limit `price` or a `trigger_price` (the trigger anchor is derived from the mark price and the offset).\n- **`TrailingLimit`** trails like `TrailingStop` but fires a limit order instead of a market order. It requires both `trailing_offset_bps` (the trailing trigger) and `limit_offset_bps` (the fire-time limit offset). It does not take `price`, `trigger_price`, or `stop_price`; the limit price is computed at fire time from the mark that crossed the offset.","required":["market_id","side","order_type","quantity","time_in_force"],"properties":{"market_id":{"type":"string"},"side":{"type":"string","enum":["Buy","Sell"]},"order_type":{"type":"string","description":"Order type. `Limit` and `Market` are unconditional. The remaining six are conditional: `StopLimit` / `StopMarket` fire when the mark price crosses `trigger_price` in the adverse direction; `TakeProfitLimit` / `TakeProfitMarket` fire on the favorable direction; `TrailingStop` fires as a market order when the mark retraces from its best-seen extreme by `trailing_offset_bps`; `TrailingLimit` fires the same way but rests a limit order priced off the fire price by `limit_offset_bps`. See the schema description for per-type field requirements.","enum":["Limit","Market","StopLimit","StopMarket","TakeProfitLimit","TakeProfitMarket","TrailingStop","TrailingLimit"]},"price":{"$ref":"#/components/schemas/Decimal","description":"Limit price. Required for limit-family orders (`Limit`, `StopLimit`, `TakeProfitLimit`); omit for market-family and trailing orders."},"quantity":{"$ref":"#/components/schemas/Decimal"},"time_in_force":{"type":"string","description":"Time-in-force policy. `PostOnly` rejects the order if it would take liquidity (cross the book) on entry, guaranteeing it rests as a maker.","enum":["GTC","IOC","FOK","PostOnly"]},"reduce_only":{"type":"boolean"},"stop_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"deprecated":true,"description":"**Deprecated** — use `trigger_price` instead. Legacy trigger threshold for the stop / take-profit family. Accepted as a fallback only when `trigger_price` is absent; when both are supplied, `trigger_price` wins. Ignored for `Limit`, `Market`, `TrailingStop`, and `TrailingLimit` orders."},"trigger_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Canonical trigger threshold for triggerable, non-trailing orders (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`), which require it (the legacy `stop_price` field is accepted as a fallback when this is omitted). Not used by `Limit`, `Market`, `TrailingStop`, or `TrailingLimit` orders."},"trailing_offset_bps":{"type":["integer","null"],"minimum":0,"description":"Trailing offset in basis points (1 bp = 0.01%). Required for `TrailingStop` and `TrailingLimit` orders; ignored for all other order types. The trailing trigger fires once the mark price retraces from its best-seen extreme by this many basis points: `TrailingStop` fires a market order, `TrailingLimit` fires a limit order priced by `limit_offset_bps`. A value of `0` is accepted and fires the trigger at the first mark-price evaluation after placement (no retracement required)."},"limit_offset_bps":{"type":["integer","null"],"minimum":0,"maximum":9999,"description":"Offset in basis points for the fired limit price (`TrailingLimit` only; required together with `trailing_offset_bps`). When the trailing trigger fires at `fire_price`, the injected limit order rests at `fire_price` * (1 + offset) for buys / * (1 - offset) for sells, tick-rounded toward the tighter bound. A value of 0 rests the limit exactly at `fire_price`. Ignored for other order types."},"stp":{"type":["string","null"],"enum":["CancelNewest","CancelOldest","DecrementAndCancel",null],"description":"Opt-in self-trade prevention. Omit the field, or send `null`, to allow self-matching — that is the default and the industry-standard behaviour, and the engine will fill your order against your own resting order. Set a mode to have the engine intervene when a taker meets a maker on the same account instead.\n\n`CancelNewest` cancels the incoming taker and leaves the maker resting; the taker stops walking the book entirely, so remaining taker size beyond that maker is cancelled too. `CancelOldest` cancels the resting maker and lets the taker carry on against other accounts' makers. `DecrementAndCancel` reduces both sides by `min(taker_remaining, maker_size)` and cancels the smaller side, leaving the larger to continue at the reduced quantity.\n\nThe check is applied **per encountered same-account maker**, not once at order entry, so a taker crossing several of your own makers is evaluated at each one. It sits in the shared matching path, so it applies to every order type rather than a subset. When a mode cancels an order, that order comes back with `cancellation_reason` set to the object `{\"Stp\": \"<mode>\"}` — see `Order.cancellation_reason`."},"client_id":{"type":["string","null"],"maxLength":128,"description":"Client-supplied idempotency key for this order, opaque to the venue and scoped to your account: two accounts may use the same value independently.\n\n**Retrying a submit with a `client_id` you have already used does not place a second order.** A client cannot tell \"the venue never received my order\" from \"it received it, accepted it, and the response was lost\" — both look like a timeout — so a retry is the only safe response to one, and this is what makes retrying safe. The repeat is answered `200` with the order the first request created, rather than `201` with a new one. Submit the retry byte-identically; the venue answers with the order it already holds and does not compare the rest of the body against it.\n\n**Retention is bounded.** The venue remembers a limited number of recent keys per account, sized to cover a retry window rather than a session. A key reused long after that window is treated as new, so it is not a durable uniqueness constraint and must not be used as one — do not rely on it to prevent a duplicate hours later.\n\nAn amend (`PATCH /orders/{order_id}`) carries the key to the replacement order, so a retry after an amend is answered with the replacement. Omit the field, or send `null`, to opt out; an empty string is treated as omitted.\n\n`POST /orders/batch` honours the key per entry, so a retried batch can mix replays of orders you already hold with genuinely new ones: a replayed entry comes back as a normal success row carrying the original order, subject to the same resting-only limit as above. Two entries in one batch sharing a key accept the first and refuse the second."},"max_slippage_bps":{"type":["integer","null"],"minimum":0,"description":"Server-enforced slippage cap in basis points (1 bp = 0.01%). Omit the field for no cap. When set, the engine captures the book mid-price `(best_bid + best_ask) / 2` once at submission and requires the order's running fill VWAP to stay inside `mid ± mid × bps / 10000` as it walks the book. Fills made before the cap binds stand; the fill that would push the VWAP outside the band is not made and the unfilled remainder is cancelled — the order comes back on the normal `201` with `status` `Cancelled` and `cancellation_reason` `SlippageCap`, not as an error. Because the reference is the mid captured server-side at submission and the bound is on the running VWAP rather than on the worst individual fill, this is a stricter guarantee than a marketable limit price derived client-side from a previously sampled reference price.\n\nApplies to the market family: `Market`, plus `StopMarket`, `TakeProfitMarket`, and `TrailingStop`, which carry the field through their conversion and have the cap applied when they fire as market orders. Accepted but ignored on `Limit`, `StopLimit`, `TakeProfitLimit`, and `TrailingLimit` — a limit order already fills at its limit price or better.\n\nTwo edges worth knowing. A mid-price requires **both** sides of the book to be non-empty, so a capped order submitted while either side is empty is rejected with `InsufficientLiquidity` (a `400` on `POST /orders`, an `err` item on `POST /orders/batch`) — including when the empty side is the order's own and the order could otherwise have filled. And `0` does not mean \"no cap\": it collapses the band onto the mid exactly, so against any book with a non-zero spread the order cancels with zero fills. `POST /orders/preview` accepts the field but does not apply it — `expected_fill_vwap` always walks the full book."}}},"Decimal":{"type":"string","description":"Arbitrary-precision decimal serialized as a string (lossless). Parse with a decimal type, never a float."},"PreviewResponse":{"type":"object","description":"Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.","properties":{"accepted":{"type":"boolean"},"reject_reason":{"type":["string","null"]},"required_initial_margin":{"$ref":"#/components/schemas/Decimal"},"projected_post_trade_equity":{"$ref":"#/components/schemas/Decimal"},"projected_post_trade_liquidation_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}]},"projected_post_trade_leverage":{"$ref":"#/components/schemas/Decimal"},"expected_fill_vwap":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}]},"projected_fees":{"$ref":"#/components/schemas/Decimal"}}},"RateLimitError":{"type":"object","description":"The body of a 429. Branch on `code` (always `RATE_LIMIT_EXCEEDED`) and on `bucket` to learn WHICH budget bottlenecked. Schematised rather than left to `examples` because `bucket` is meant to be read by machines: an example gives a generated client no field and a validator nothing to check `\"bucket\": \"ownr\"` against. `bucket` here and the `X-RateLimit-Bucket` header carry the same value and the same enum.","required":["code","message","bucket"],"properties":{"code":{"type":"string","enum":["RATE_LIMIT_EXCEEDED"],"description":"Always `RATE_LIMIT_EXCEEDED` on this response. The other 429s this API can return carry different codes (`too_many_agents`, `rate_limited`, `cap_exceeded`, `daily_limit_exceeded`) and are not this shape."},"message":{"type":"string","description":"A diagnostic whose wording is NOT stable. Do not parse it; branch on `bucket`."},"bucket":{"type":"string","enum":["key","owner","order","cancel","ip","login"],"description":"Which budget refused this request. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The buckets are independent, so a caller at its read ceiling can still submit orders, and a caller at its submission ceiling can still cancel — do not back off the whole client on any one of them."},"tier":{"type":"string","description":"The rate-limit tier the refused request was charged against — `public` for the per-IP bucket, otherwise the authenticated key's tier. Absent on responses from paths that refuse before a tier is resolved."}}}},"responses":{"Unauthorized":{"description":"Authentication failed. All 401 responses return the same opaque body to prevent information leakage. A credential minted on a different network is one of the causes it hides — by design, so that a key id cannot be probed for existence on another network. Do not retry it against another host.","content":{"application/json":{}}},"RateLimited":{"description":"Rate limit exceeded. **Retryable** — honour `retry-after` and back off; this is what separates a `429` from the permanent `403` jurisdiction refusals. Branch on the body's `code`, which is always `RATE_LIMIT_EXCEEDED`, and on `bucket` to learn WHICH budget bottlenecked — the buckets are independent, so a caller at its read ceiling can still submit orders and should not back off the whole client. Do not parse `message`: it is a diagnostic whose wording is not stable. Requests are charged by weight, so exhausting a budget takes fewer heavy calls than `x-ratelimit-limit` suggests. See “Rate limits” in the API description.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Budget of request weight per second for your tier — not a count of requests. Sent on successful responses too."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Tokens left, in unit-cost requests: 10 means ten weight-1 requests or two weighing 5. Sent on successful responses too. Always `0` on this response."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp (seconds) when the budget is whole again. Sent only on a `429`, never on a success."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying, never below 1. Derived from the **weighted** cost of the refused request, so it is not in the same unit as `X-RateLimit-Remaining`. Sent only on a `429`."},"X-RateLimit-Bucket":{"schema":{"type":"string","enum":["key","owner","order","cancel","ip","login"]},"description":"Which budget refused this request, mirroring the body's `bucket`. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The read, trading and cancellation budgets are counted separately, so a `429` on one does not mean the others are exhausted — in particular a `429` naming `order` never means your cancels are refused. Sent only on a `429`."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}}}},"paths":{"/orders/preview":{"post":{"tags":["Trading"],"summary":"Preview an order","description":"Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.","operationId":"previewOrder","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"200":{"description":"Projected pre-trade impact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewResponse"}}}},"400":{"description":"Validation error"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}
````

**Test it** cannot sign this request. `X-Signature` is an HMAC computed per call: see [Authentication](/api-reference/guides/authentication.md), or use an SDK.

***

<sub>API version</sub> <sub></sub><sub>`0.9.131`</sub>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.nexus.xyz/api-reference/trading/preview-order.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
