> ## Documentation Index
> Fetch the complete documentation index at: https://perps-api.vooi.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Broker API

> How brokers onboard users, manage their keys, and pull trading statistics

This section is for teams building their own trading product on VOOI. If you just want to trade, sign up on [Ultra](https://ultra.vooi.io) or use an existing broker's app.

Brokers provision users on VOOI and read their trading statistics. Users hold their own private keys and session tokens — brokers never see either.

<Info>
  Access is invite-only. Contact our team to get a broker key.
</Info>

## Trust model

VOOI is authentication-agnostic by design. You — the broker — are the sole authority over user identity. Whatever auth method your users see (wallet signature, email, SSO, Passkeys, or carrier pigeon) lives entirely in your app; VOOI neither implements nor sees those flows.

The contract between your backend and VOOI is one Ed25519 keypair per user. You generate or collect it however fits your auth model, register the public half with VOOI, and VOOI then executes for whoever signs with the matching private key. VOOI has no opinion about who that signer is.

Two consequences to surface to your own users:

* **You can fully impersonate any of your users.** [`PUT /broker/users/public-key`](/docs/api-reference/broker/set-or-remove-a-public-key-for-a-user) lets you rotate any of your users' keys to one you control, after which [`POST /user/tokens`](/docs/api-reference/tokens/create-a-new-api-token) authenticates as them. This is a direct consequence of you being the identity authority, not a gap — users trusting your app are trusting you accordingly.
* **Mapping your external identities to `userId`s is your job.** VOOI exposes no lookup by wallet address, email, public key, or anything other than `userId` itself. Persist the `userId` against whatever external identity your auth flow produces; on reconnect or re-login, reuse it rather than re-registering.

## What brokers can and can't do

Brokers are read-only on statistics and write-only on public keys:

* **Can** register users, set or rotate their Ed25519 public keys, configure per-exchange builder / integrator defaults, and pull their trading statistics (orders and trades).
* **Cannot** see user session tokens, private keys, wallet addresses, account balances, positions, or any data a user holds privately.

## Flow

The end-to-end flow for onboarding and signing in a user:

<Steps>
  <Step title="Obtain an Ed25519 keypair for the user">
    Derive it from a wallet signature, generate it server-side, or let the user generate and keep it — whichever fits your auth model (see [Trust model](#trust-model)). You only need the public half for registration.
  </Step>

  <Step title="Register the user">
    Your backend calls [`POST /broker/users`](/docs/api-reference/broker/register-a-new-user) with the public key. VOOI returns a `userId` (UUID) that belongs to the user forever.
  </Step>

  <Step title="(Optional) Rotate the public key later">
    Call [`PUT /broker/users/public-key`](/docs/api-reference/broker/set-or-remove-a-public-key-for-a-user) whenever the user regenerates their keypair. Rotation automatically revokes that user's unnamed session tokens.
  </Step>

  <Step title="User signs requests and obtains a token">
    The user signs a message with the private key and calls [`POST /user/tokens`](/docs/api-reference/tokens/create-a-new-api-token) to get a `vooi_…` bearer token. See [API Tokens](/docs/tokens) for the signature format.
  </Step>

  <Step title="User trades; broker observes">
    The user places orders, opens positions, and trades against VOOI directly. Your backend reads their statistics from [`GET /broker/orders`](/docs/api-reference/broker/get-order-history-for-broker-users) and [`GET /broker/trades`](/docs/api-reference/broker/get-trade-history-for-broker-users).
  </Step>
</Steps>

## Quickstart

Authenticate every `/broker/*` request with your broker key in the `X-Broker-Key` header. Keep this key server-side — never ship it in a client app.

### 1. Register a user

```bash theme={null}
POST https://perps-api.vooi.io/broker/users
X-Broker-Key: <your-broker-key>
Content-Type: application/json

{
  "publicKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}
```

Store the returned `id` — it's the user's permanent UUID:

```json theme={null}
{
  "id": "0191f6b3-2d4a-7c8e-9f01-2a3b4c5d6e7f",
  "broker": "your-broker-slug"
}
```

<Note>
  **Registration is not idempotent.** Every successful `POST /broker/users` call returns a new `userId`. Dedupe on your side: map whatever identity your auth flow produces (wallet address, email, SSO subject, …) to the `userId` VOOI returned, and only call once per user. On reconnect or re-login, look up the existing `userId` from your mapping and reuse it — VOOI has no recovery path if you lose it.

  `publicKey` is optional in the request. A user registered without one cannot mint tokens — [`POST /user/tokens`](/docs/api-reference/tokens/create-a-new-api-token) returns `400 Public key not set` until you attach one with the `PUT` call below.
</Note>

### 2. Rotate the public key (when needed)

```bash theme={null}
PUT https://perps-api.vooi.io/broker/users/public-key
X-Broker-Key: <your-broker-key>
Content-Type: application/json

{
  "userId": "0191f6b3-2d4a-7c8e-9f01-2a3b4c5d6e7f",
  "publicKey": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3"
}
```

Responds `204 No Content` on success. Pass `"publicKey": null` to clear it (the user will not be able to mint new tokens until a key is set again). Submitting the user's existing key is a no-op — no tokens are touched.

Rotation affects that user's outstanding tokens as follows:

* **Session tokens (unnamed) are deleted.** The next request carrying one gets `401 Invalid or expired session`.
* **Named tokens are left alone.** They keep working until their own `expiresAt`. If you need a hard re-auth, enumerate them with the user's bearer via [`GET /user/tokens`](/docs/api-reference/tokens/list-named-api-tokens) and delete each one.
* **Historical orders, trades, and positions are untouched.** Rotation only changes signature verification for future `createToken` / `deleteToken` calls.

### 3. Read user activity

All broker read endpoints use cursor pagination and share a `{ cursor, items }` envelope — `cursor` is `null` on the final page; pass it back unchanged on the next request.

* [`GET /broker/orders`](/docs/api-reference/broker/get-order-history-for-broker-users) and [`GET /broker/trades`](/docs/api-reference/broker/get-trade-history-for-broker-users) — filters: `userId`, `exchange`, `from`, `to`, `cursor`, `limit` (1–1000, default 100). Ordered newest first; ties are broken deterministically so cursor paging stays stable. When both `from` and `to` are set, the window must not exceed 30 days.

```bash theme={null}
GET https://perps-api.vooi.io/broker/orders?userId=0191f6b3-2d4a-7c8e-9f01-2a3b4c5d6e7f&limit=200
X-Broker-Key: <your-broker-key>
```

### 4. Read statistics

Several endpoints cover stats — they answer different questions:

| Endpoint | Scope | Granularity | Pagination |
| - | - | - | - |
| [`GET /broker/stats`](/docs/api-reference/broker/get-aggregated-trading-statistics-for-broker-users-max-30-day-period) | Aggregate across all of your users for one period | One breakdown object per supported exchange (`volumeUsd`, `uniqueTraders`, `orderCount`) | None |
| [`GET /broker/users/stats`](/docs/api-reference/broker/get-broker-users-ranked-by-executed-notional-volume) | Leaderboard of *your* users by executed volume for one period | One row per user (`userId`, `volume`, `tradesCount`) | Cursor, `limit` 1–50 (default 50) |
| [`GET /broker/users/top-volume`](/docs/api-reference/broker/get-broker-users-ranked-by-api-created-order-volume-in-usd-with-pagination) | Leaderboard of *your* users by API-created order volume, over a longer window | One row per user (`userId`, `volume`) | Cursor, `limit` 1–1000 (default 100) |
| [`GET /broker-statistics/daily-orders`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-count-of-orders-created-via-api-with-gaps-filled) | Daily count of orders created via the API | One row per calendar day (`date`, `amount`); `totalValue` across the range | None |
| [`GET /broker-statistics/daily-traders`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-count-of-unique-traders-from-api-created-orders-with-gaps-filled) | Daily count of unique traders | Same | None |
| [`GET /broker-statistics/daily-volume`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-trade-volume-from-api-created-orders-in-usd-with-gaps-filled) | Daily trade volume in USD | Same | None |

`GET /broker/stats` and `GET /broker/users/stats` require `from` and `to` as Unix milliseconds, **hard-reject** with `400 Bad Request` if `to - from > 30 days`, and (for `users/stats`) reject `to` set in the future. Ranges crossing the 30-day boundary must be split into multiple calls.

`GET /broker/users/top-volume` is the long-window companion to `users/stats`: it takes ISO dates (default the last 30 days, maximum range **180 days**, not earlier than `2026-01-01`), pages up to `limit` 1–1000 (default 100), and accepts an optional `exchanges` filter. Reach for `users/stats` when you need `tradesCount` or a strict 30-day executed-volume window, and `users/top-volume` when you want a deeper leaderboard across a longer period.

The three `/broker-statistics/daily-*` endpoints take `from` and `to` as **Unix milliseconds**, both inclusive. `from` defaults to 30 days back (1 day at hour resolution), `to` defaults to now, and `from` cannot be earlier than `2026-01-01`.

They also take a `resolution` of `day` (UTC calendar day, the default) or `hour` (UTC clock hour). `from` and `to` snap to interval boundaries, and `hour` limits the range to 7 days. Missing intervals come back as `amount: 0`, so `items` always has one row per interval in the window.

Optional filters: `exchange` narrows to one venue, `baseSymbolPrefix` and `baseSymbolNotPrefix` keep or drop trades by base-symbol prefix (case-insensitive — useful for isolating a Hyperliquid HIP-3 DEX such as `xyz:`), and `usersStoplist` excludes up to 100 user IDs.

#### Per-user and per-list statistics

The endpoints above aggregate across all of your users or rank them. Two other shapes are available when you have specific users in hand. All of them share the `from` / `to` / `resolution` conventions described above.

For **one** user, by query parameter (`userId`):

* [`GET /broker-statistics/user-volume`](/docs/api-reference/broker-statistics/get-user-trade-volume-and-fees-broken-down-by-exchange) — that user's trade volume and fees, split by exchange. Set `totalAccountStats=true` to count the whole exchange account's volume rather than only volume from orders created through this API.
* [`GET /broker-statistics/user-pnl`](/docs/api-reference/broker-statistics/get-user-realized-pnl-broken-down-by-exchange) — that user's realized PnL, split by exchange.
* [`GET /broker-statistics/user-volume/chart`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-trade-volume-of-a-user-broken-down-by-exchange-with-gaps-filled) and [`GET /broker-statistics/user-pnl/chart`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-realized-pnl-of-a-user-broken-down-by-exchange-with-gaps-filled) — the same figures rolled up per interval, with gaps filled.

For a **list** of users, by JSON body carrying a `usersList` of 1–100 entries (each an `id` plus an optional `startedFrom` timestamp clamping how far back that user's trades are counted):

* [`POST /broker-statistics/users-volume`](/docs/api-reference/broker-statistics/get-per-user-trade-volume-and-broker-fees-broken-down-by-exchange-for-a-list-of-users) — per-user executed notional, broker fees, and trade fees, split by exchange.
* [`POST /broker-statistics/users-volume/chart`](/docs/api-reference/broker-statistics/get-per-interval-day-or-hour-trade-volume-broken-down-by-exchange-across-a-list-of-users-with-gaps-filled) — the same figures rolled up per interval across the whole list, with gaps filled.

Both accept an optional `exchanges` array to count trades only on those venues. Excluded venues still appear in the response with zero amounts.

### Broker vs user read scope

Brokers read only what these endpoints return: aggregate and per-day stats, per-user stats, and (userId-filterable) order / trade history. Per-user **positions, balances, open orders, and account settings** are not exposed on the broker API — that data lives behind the user's bearer token on the [`/exchange`](/docs/api-reference/exchange/get-open-positions) routes.

## Configure exchange defaults

Each venue passes a "broker" record on every order — a builder address (or integrator account index for Lighter and Robinhood) plus fees in basis points. Binance and MEXC are the exceptions: their broker records are attribution identifiers only, with no fee fields. Brokers can register one default per venue so that orders from any of their users automatically carry those values.

When a default is set, it **overrides** the per-order `broker` field on [`POST /exchange/orders`](/docs/api-reference/exchange/create-a-new-order) — your users cannot opt out from the client side. Without a default, the order's own `broker` is used (or no builder if it was omitted).

Settings are **versioned rather than edited in place.** Each write adds a record effective from that moment; the most recent record is the one in force, and statistics apply whichever record was active when each order was created. Past figures therefore stay stable when you change your fees.

* [`POST /broker/exchanges`](/docs/api-reference/broker/add-new-broker-key-and-fee-settings-for-a-specific-exchange) — add a settings record for one venue, effective now. Responds `201`. Body fields:
  * `exchange` — `aster`, `hyperliquid`, `lighter`, `robinhood`, `extended`, `binance`, or `mexc`.
  * `identifier` — the EVM builder address (Aster, Hyperliquid), the integrator account index (Lighter, Robinhood), or, on Extended, the Builder clientId from the Extended UI API page (a positive-integer string, not an EVM address or account index). On Binance, `identifier` is an attribution tag (up to 24 characters from `A–Z a–z 0–9 . / : _ -`) prepended to auto-generated client order IDs. On MEXC it is the Broker\_ID MEXC issued you. Neither Binance nor MEXC takes fee fields.
  * `marketFeeBps` — fee in basis points, as a positive decimal string, charged on orders the venue executes as **market** orders. Trigger orders count as market on Hyperliquid, Aster, and Ondo.
  * `limitFeeBps` — the same, for orders the venue executes as **limit** orders. On Lighter and Robinhood the two are applied as the taker and maker fees respectively.
  * `maxFeeBps` — fee cap in basis points, used as the default during user-side approval. Both `marketFeeBps` and `limitFeeBps` must be ≤ `maxFeeBps`; the call returns `400` otherwise. On Hyperliquid this becomes the cap baked into the [user's `approveBuilderFee` signature](/docs/hyperliquid-account#approve-a-builder-fee), so raising the cap later requires the user to re-sign. On Aster, the fees still cannot exceed the cap baked into the user's [Aster registration](/docs/register-aster#customize-the-builder-approval).
* [`GET /broker/exchanges`](/docs/api-reference/broker/get-broker-exchange-settings-for-all-exchanges) — list the settings currently in force. Each row carries `identifier`, `marketFeeBps`, `limitFeeBps`, and `maxFeeBps`; the fee fields are `null` on venues that have no builder fee.
* [`DELETE /broker/exchanges/{exchange}`](/docs/api-reference/broker/deactivate-broker-settings-for-a-specific-exchange) — deactivate the venue. Responds `204 No Content`. The exchange stops being listed by `GET /broker/exchanges`, new orders carry no builder attribution, and builder approval falls back to caller-supplied `identifier` and `maxFeeBps`. Earlier records remain in effect for past statistics.

### Approve flow fallback

A user trading through your app only has to run the user-side approve flow when your broker actually uses builder fees on a venue — see [Approve a builder fee](/docs/hyperliquid-account#approve-a-builder-fee) on Hyperliquid. One pair of endpoints serves all supported venues:

* [`POST /exchange/broker/prepare`](/docs/api-reference/exchange-broker/build-the-payload-the-user-must-sign-to-approve-a-broker-on-the-given-exchange) builds the data the user signs.
* [`POST /exchange/broker/execute`](/docs/api-reference/exchange-broker/submit-a-signed-broker-approval-action-for-the-given-exchange) submits the signature.
* [`GET /exchange/broker/approved`](/docs/api-reference/exchange-broker/get-the-configured-and-on-chain-approved-builder-fees-for-the-user-s-broker) reports whether a given builder/integrator is already approved.

Pass `exchange` (`hyperliquid`, `lighter`, `robinhood`, or `aster`) in the body. When a default is configured here, the user-side prepare call can omit `identifier` and `maxFeeBps` and picks them up from the broker record; if they are passed explicitly they must match the configured defaults exactly, or the call returns `400`. Without a broker default, both fields are required — typically the case for users connecting their own wallets directly.

The signer differs by venue: Hyperliquid and Aster expect an EIP-712 signature; Lighter and Robinhood expect a hex message signed with an Ethereum wallet. For Aster, an account that approved a builder during [Aster registration](/docs/register-aster#customize-the-builder-approval) can re-run this flow later to raise the cap or rotate the builder without re-registering.

Every other venue is an exception: none of Extended, Binance, Bybit, MEXC, or Ondo needs a user-side builder-fee approval. Extended's builder code applies automatically per order, and Binance and MEXC attribution is identifier-only — so the approve endpoints accept only `hyperliquid`, `lighter`, `robinhood`, and `aster`.

## API reference

Full request and response schemas for every broker endpoint are in the [API reference](/docs/api-reference).

## Next steps

<CardGroup cols={2}>
  <Card title="End-user API tokens" icon="key" href="/docs/tokens">
    How your users sign requests and obtain session tokens
  </Card>

  <Card title="Connecting exchanges" icon="plug" href="/docs/exchanges">
    Aster, Hyperliquid, Lighter, Robinhood, Extended, Binance, Bybit, MEXC, and Ondo key setup
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.