# HTTP — `POST /v1/exchange`

One request carries one action type: a batch of up to 20 places, modifies, or
cancels, a mass cancel, a deposit request, or a withdrawal request. Every order
names its `venue`; Mosaiq never reroutes it.

Separate requests are independent intents. Mosaiq does not order execution,
venue send, or finality between two requests, even from the same user. A
request that is not durably accepted before a crash is not sent to a venue.
An accepted request runs to its end (journal, projection, venue send) even when
the client disconnects before the response arrives, so read the outcome back
through `orderUpdates` or `openOrders` instead of assuming the action was
dropped.

```http
POST /v1/exchange
Authorization: Bearer <your-api-key>
```

| Action | Auth |
| --- | --- |
| `place`, `modify`, `cancel`, `mass_cancel` | API key |
| `deposit` | API key or Para JWT |
| `withdraw` | Para JWT (`X-Tessera-Wallet` when the JWT holds several EVM wallets) |

## Disconnects and shutdown

Closing the HTTP connection discards only the response. Mosaiq continues an
accepted request without the client. During gateway shutdown, a request can
stop if it has not reached its first durable or ordering-visible action. After
that action starts, Mosaiq continues the whole request to completion while its
dependencies remain available.

If the connection closes before the response arrives, treat the result as
unknown. Reconcile order actions by `cloid` before you retry them.

## Place

A placement is a limit order when it carries `p` and `t`, and a market order
when it carries `slippage` instead. Mixing `slippage` with `p` or `t`, or
sending only one of `p` and `t`, is rejected with `unsupported`.

Limit `t` is `Ioc` or `Alo` (post-only). `Gtc` parses but is rejected with
`unsupported_tif`.

```jsonc
{
  "type": "place",
  "orders": [
    {
      "a": "BTC",
      "b": "buy",
      "p": "65000",
      "s": "1",
      "venue": "lighter",
      "t": "Alo",
      "c": "0x00000000000000000000000000000002"
    }
  ]
}
```

A market order is a bounded IOC. `slippage` is a fraction in `(0, 0.5]`; the
venue-side limit is the venue mid price moved by that fraction. Margin is
reserved at the Mosaiq oracle price moved by the same fraction.

```jsonc
{
  "type": "place",
  "orders": [
    {
      "a": "BTC",
      "b": "buy",
      "s": "1",
      "slippage": "0.01",
      "venue": "hyperliquid",
      "c": "0x00000000000000000000000000000001"
    }
  ]
}
```

### Reduce-only

`reduceOnly: true` requires a current position on the selected venue and a
reducing side (sell against a long, buy against a short). A flat position or
the wrong side is rejected with `reduce_only` before venue dispatch.

An oversized request is clamped to the reducible position rather than
rejected. Existing reduce-only orders are not subtracted when computing that
clamp. Newer placements and size increases take capacity first and can
permanently trim or cancel older reduce-only orders on the same user, asset,
venue, and side. A position decrease trims them too. A trim is reverted only
when the placement that caused it is rejected before it is sent to the venue.

Mosaiq re-applies the venue size increment and minimums after clamping; if the
result is not a valid venue order the item is rejected with `min_notional`.
This applies to `Ioc` and market reduce-only orders as well, so an immediate
order can displace older resting protection even when it does not fill. The
accepted response returns the clamped `size`; reconcile later values from
`openOrders` or `orderUpdates`.

```jsonc
{
  "type": "place",
  "orders": [
    {
      "a": "BTC",
      "b": "sell",
      "p": "65000",
      "s": "0.5",
      "venue": "hyperliquid",
      "t": "Ioc",
      "reduceOnly": true,
      "c": "0x00000000000000000000000000000005"
    }
  ]
}
```

## Cancel

Cancels identify the order by `venue` and `c`. The asset is resolved from the
original placement. Venue order IDs are never exposed.

If a cancel for the same order is already durable and unresolved, another
cancel reuses that action. Mosaiq does not create or send a second cancel, and
the HTTP request waits for the existing action's terminal result. If a retry
reaches the short interval after the first cancel claims the order but before
its action becomes durable, the endpoint still returns HTTP 200. The affected
item has `data[].status` set to `error` and `data[].code` set to
`in_flight_admission_conflict`. Retry the cancel.

```jsonc
{
  "type": "cancel",
  "orders": [
    { "venue": "lighter", "c": "0x00000000000000000000000000000002" }
  ]
}
```

## Modify

`modify` replaces the price and total size of a resting limit order by `c`, on
Hyperliquid or Lighter. `a` and `b` must match the original placement. Order
type and time-in-force cannot change and are read from the existing order;
sending `t` is an unknown field and fails with HTTP `422 invalid_request`.
Modifying a market order fails with `unsupported_order_type`; an `s` below the
already-filled size fails with `bad_request`.

For a reduce-only order, increasing `s` takes new allocation priority and the
requested size is clamped to the reducible position. A price-only modify or a
size decrease keeps its priority, and trimmed size stays trimmed.

```jsonc
{
  "type": "modify",
  "orders": [
    {
      "venue": "hyperliquid",
      "c": "0x00000000000000000000000000000005",
      "a": "BTC",
      "b": "buy",
      "p": "63000",
      "s": "1"
    }
  ]
}
```

## Mass cancel

`mass_cancel` cancels every open order of the authenticated user. `venues`
restricts it; omit it or pass `[]` for all venues. An unknown venue code fails
the whole request with `unknown_venue`.

```jsonc
{ "type": "mass_cancel", "venues": ["hyperliquid"] }
```

The response `data` holds one result per open order that was targeted, in no
guaranteed order. A user with nothing open gets `"data": []`. The 20-action cap
does not apply.

## Deposit

A deposit is USDC on Arbitrum from the registered wallet to the returned
treasury address. The request is tracked for one hour; the minimum is 1 USDC.
Mosaiq credits the account only after observing a transfer whose sender is the
registered wallet and whose amount equals `amount` exactly, before
`expiresAtMs`. Any other transfer to the treasury is held for operator review.
Amounts above the operator's auto-credit cap are also held for review.

```jsonc
{ "type": "deposit", "amount": "100" }
```

```jsonc
{
  "status": "ok",
  "data": [
    {
      "status": "accepted",
      "requestId": "...",
      "kind": "deposit",
      "amount": "100",
      "chain": "arbitrum",
      "from": "0x...",
      "to": "0x...",
      "expiresAtMs": 1780003600100
    }
  ],
  "server_time_ms": 1780000000100,
  "server_receive_time_ms": 1780000000088
}
```

## Withdraw

`withdraw` creates a tracked request to the registered wallet; it is not an
instant payout. The amount must not exceed the lower of `totalCashUsdc` and
`availableFunds`, and only one request per user can be pending; otherwise the
request fails with `insufficient_available_funds`. A rolling per-user cap,
10,000 USDC per hour by default, counts pending and completed requests in the
window and returns HTTP `429 withdraw_limit_exceeded`.

```http
Authorization: Bearer <para-jwt>
X-Tessera-Wallet: 0x... # required when the JWT has multiple EVM wallets
```

```jsonc
{ "type": "withdraw", "amount": "100" }
```

```jsonc
{
  "status": "ok",
  "data": [
    {
      "status": "accepted",
      "requestId": "...",
      "kind": "withdraw",
      "amount": "100",
      "chain": "arbitrum",
      "destination": "0x..."
    }
  ],
  "server_time_ms": 1780000000100,
  "server_receive_time_ms": 1780000000088
}
```

Track both request kinds through [`GET /v1/funding`](/api/funding).

## Batches

`orders` carries up to 20 actions of one kind: places (limit and market, any
venues), modifies, or cancels. Items are processed in array order, so an
earlier place reserves margin before a later one. `data` answers item by item,
by index. An empty `orders` array fails the request with `bad_request` (place,
modify) or `invalid_cancel` (cancel); more than 20 items fails it with
`batch_too_large` and nothing is processed.

```jsonc
{
  "type": "place",
  "orders": [
    { "a": "BTC", "b": "buy", "p": "65000", "s": "1", "venue": "lighter", "t": "Alo", "c": "0x00000000000000000000000000000003" },
    { "a": "ETH", "b": "sell", "s": "0.1", "slippage": "0.01", "venue": "hyperliquid", "c": "0x00000000000000000000000000000004" }
  ]
}
```

## Fields

| Field | Type | Description |
| --- | --- | --- |
| `type` | `string` | `place`, `modify`, `cancel`, `mass_cancel`, `deposit`, or `withdraw`. |
| `a` | `string` | Canonical asset from `data.universeData.universe[*].name`. Place and modify. |
| `b` | `string` | `buy` or `sell`. |
| `p` | `string` | Limit price. Required with `t`; absent on market orders. |
| `s` | `string` | Size in base units. |
| `slippage` | `string` | Market-order slippage fraction in `(0, 0.5]`, e.g. `"0.01"`. |
| `venue` | `string` | Venue code from `data.universeData.universe[*].venues[*].venue`. Required. |
| `venues` | `array` | `mass_cancel` filter; empty means all. |
| `t` | `string` | `Ioc` or `Alo`. Required on limit placements; not accepted on modify. |
| `reduceOnly` | `boolean` | Place only. See [Reduce-only](#reduce-only). |
| `c` | `string` | Client order id. Optional on place, required on modify and cancel. |
| `amount` | `string` | USDC amount for `deposit` and `withdraw`. |

`c` is `0x` followed by exactly 32 lowercase hexadecimal characters and must
not be all zeros. Uppercase is rejected. If omitted on a REST placement, Mosaiq
generates one and returns it. A user cannot hold two open orders with the same
`c`; a repeated `c` inside one place or cancel batch fails the later item with
`duplicate_cloid`. Modify batches are not deduplicated.

Decimal values are JSON strings in both directions.

## Precision and constraints

Clients send decimal strings for `p` and `s` and never encode venue-native
formats. Each venue market publishes its constraints in
[`data.universeData.universe[*].venues[*].constraints`](/api/clearinghouse-state):

| Field | Meaning |
| --- | --- |
| `sizeDecimals`, `sizeIncrement` | Size precision and step. |
| `priceDecimals`, `tickSize` | Price precision and grid. |
| `minNotional`, `minBaseAmount` | Minimum quote notional and base size, when the venue has them. |
| `maxBaseAmount`, `maxLimitNotional`, `maxMarketNotional` | Maximums; exceeding one fails with `bad_request`. |
| `supportsReduceOnly`, `supportsPostOnly`, `requiresIsolatedMargin` | Venue capabilities. |

Mosaiq fits the request to the selected venue before sending it:

* `s` rounds down to the venue's size precision and increment;
* a buy limit `p` rounds down and a sell limit `p` rounds up, so the price
  never becomes more aggressive;
* a size that rounds to zero fails with `bad_request`, a price that rounds to
  zero with `invalid_price`, a price that cannot be placed on the tick grid with
  `invalid_precision`, and a notional below the minimum with `min_notional`.

Only the affected item is rejected. Mosaiq remains the authority at acceptance
time; use `constraints` for client-side prevalidation and sizing.

## Response

`data` is an array aligned by index with `orders` (see
[Mass cancel](#mass-cancel) for the exception). Each item carries a `status`:

* `accepted`: the action passed Mosaiq admission and the venue acknowledged it.
  Placements and modifies carry `orderId`, `cloid`, `venue`, and the effective
  `size`; funding requests carry their request metadata. This is not lifecycle
  finality.
* `cancelled`: the venue confirmed the cancel; carries `cloid` and `venue`. A
  cancel that stops a placement before it reaches the venue answers `accepted`
  with `orderId`, `cloid`, and `venue`; the placement's own response item
  reports `preemptive_cancel`.
* `error`: carries `code`, `msg`, and `cloid` when known. Other items in the
  batch are unaffected. A code ending in `_uncertain` or `_timeout` means the
  outcome is unknown: reconcile through `orderUpdates` or `openOrders` before
  acting on that `cloid` again. Any other code means nothing changed for that
  item.

```jsonc
// success
{
  "status": "ok",
  "data": [
    {
      "status": "accepted",
      "orderId": "...",
      "cloid": "0x00000000000000000000000000000002",
      "venue": "hyperliquid",
      "size": "1"
    }
  ],
  "server_time_ms": 1780000000100,
  "server_receive_time_ms": 1780000000088
}

// request-level error: no data[] is returned
{
  "status": "error",
  "data": { "code": "...", "msg": "..." },
  "server_time_ms": 1780000000100
}
```

The top-level `status` is `ok` once the request itself was processed, even if
every item is an `error`. Request-level errors cover authentication, rate
limits, malformed bodies, empty or oversized batches, an unknown
`mass_cancel` venue, and funding failures. Risk-engine, recovery, and halt
rejections are item-level for `place` and `modify`. One Lighter failure mode
is also request-level: when a `place` or `modify` item reports the Lighter
transaction quota exhausted, the whole response becomes HTTP `503` with
`rate_limited` or `venue_unavailable`, even for items already sent. Cancel
batches return their item results instead.

### Error codes

Request-level:

| Code | HTTP | Meaning |
| --- | --- | --- |
| `unauthorized` | 401 | Missing or invalid bearer token, including `withdraw` sent with an API key. |
| `not_whitelisted`, `not_registered` | 403 | Wallet not allowlisted, or no account yet. |
| `wallet_required`, `wallet_not_connected` | 400 / 403 | `X-Tessera-Wallet` missing or not held by the JWT. |
| `invalid_json`, `unsupported_media_type` | 400 / 415 | Body is not JSON. |
| `invalid_request` | 422 | Body parses but has unknown or ill-typed fields. |
| `bad_request` | 400 | Generic validation failure, including an empty batch, deposit amounts below 1 USDC, and withdrawal amounts of zero or less. |
| `invalid_cancel` | 400 | Empty cancel batch. |
| `batch_too_large` | 400 | More than 20 actions. |
| `unknown_venue` | 400 | Unknown venue code in `mass_cancel.venues`. |
| `insufficient_available_funds` | 403 | Withdrawal exceeds withdrawable funds, or another withdrawal is pending. |
| `withdraw_limit_exceeded` | 429 | Rolling withdrawal cap reached. |
| `rate_limited` | 429 | HTTP or exchange budget exceeded, or HTTP `503` when a Lighter transaction quota is exhausted. |
| `treasury_not_configured` | 503 | `deposit` while no treasury address is configured. |
| `operations_halted` | 403 | `withdraw` during a platform-wide trading halt. |
| `risk_engine_stalled` | 503 | `withdraw` while the risk engine is stalled; item-level on `place` and `modify`. |
| `risk_core_unavailable`, `gateway_unavailable` | 503 | Risk core or durable journal unavailable; the request was not sent to a venue. |
| `internal_error` | 500 | Unexpected failure; also item-level. |

Item-level (`data[].code`):

| Code | Meaning |
| --- | --- |
| `unknown_asset`, `unknown_venue`, `symbol_mapping_missing`, `inactive_market` | The market is not tradable on that venue. |
| `unsupported` | Ambiguous placement: `slippage` with `p`/`t`, `p` or `t` alone, or none of them. |
| `unsupported_tif` | `t` is not `Ioc` or `Alo`. |
| `unsupported_order_type` | Modify targets a market order. |
| `invalid_slippage` | `slippage` outside `(0, 0.5]`. |
| `invalid_cloid` | `c` is not lowercase non-zero 128-bit hex. |
| `duplicate_cloid` | Open order with this `c`, or `c` repeated in the batch. |
| `bad_request`, `invalid_price`, `invalid_precision`, `min_notional` | Size or price failed fitting or limits. `invalid_price` also covers a limit price outside the venue price band around the oracle price. |
| `market_metadata_missing` | The venue market lacks metadata needed to build the order. |
| `cap_exceeded` | Exceeds a `userCaps` limit or available margin. |
| `margin_unavailable`, `oracle_unavailable` | Mosaiq cannot price margin for the market. |
| `venue_unavailable` | Venue state needed for admission is missing or stale, the venue send failed before dispatch, or Lighter execution is degraded after quota exhaustion. |
| `reduce_only` | No reducible position, or the reduce-only ceiling is exceeded while another replacement is unresolved. |
| `order_not_found_for_cancel` | `c` does not match a known order (cancel and modify). |
| `in_flight_admission_conflict` | Another cancel has claimed the order but is not durable yet. Retry the cancel. |
| `place_rejected`, `modify_rejected`, `cancel_rejected` | The venue rejected the action. |
| `place_failed`, `modify_failed`, `cancel_failed` | The venue send failed after acceptance. |
| `preemptive_cancel` | The placement was stopped by a cancel that became durable before the venue send. |
| `cancel_not_on_book` | The venue reports the order is no longer resting. |
| `place_uncertain`, `modify_uncertain`, `cancel_uncertain`, `cancel_timeout` | No usable venue feedback; reconcile before retrying. |
| `venue_recovering`, `account_recovering` | The venue, or a venue where the account has exposure, is being rebuilt; see `data.universeData.meta.recovery`. |
| `risk_engine_stalled`, `risk_core_unavailable` | The risk engine cannot admit the item; retry later. |
| `execution_lane_quarantined` | Execution on that venue account is quarantined pending reconciliation. |
| `execution_risk_control_blocked` | Risk-increasing execution on that venue is blocked by a venue safety control. |
| `internal_error` | Unexpected failure. During a platform-wide trading halt, `place` and `modify` items other than immediate reduce-only placements fail with this code. |
