For the complete documentation index, see llms.txt. This page is also available as Markdown.

WebSocket

Perpl streams real-time market data and account/trading data over two WebSocket (WSS) endpoints. Market data is public; trading and account data require authentication with an API key.

All frames are JSON. Prices and sizes are transmitted as scaled integers — divide by the market's price_decimals / size_decimals (from MarketConfig) to recover human-readable values. Leverage is in hundredths (1000 = 10x).

Endpoints

Endpoint
Purpose
Authentication

/ws/v1/market-data

Public market data

None

/ws/v1/trading

Trading and account data

Required (API key)

Base URL comes from the PERPL_WS_URL environment variable. Unlike the REST base URL, the WebSocket URL has no /api prefix.

Network
WebSocket base URL
Chain ID

Mainnet (default)

wss://app.perpl.xyz

143

Testnet

wss://testnet.perpl.xyz

10143

Full URLs:

  • Market Data: ${PERPL_WS_URL}/ws/v1/market-data

  • Trading: ${PERPL_WS_URL}/ws/v1/trading

Approximate rate limits are ~50 messages/second per connection and ~5 connections per IP (market-data and trading combined). Monitor for disconnects and back off if you are throttled.

Message Format

Every frame carries a common header. The message type (mt) determines the rest of the frame's shape.

interface MessageHeader {
  mt: number;       // Message type (see table below)
  sid?: number;     // Subscription ID
  sn?: number;      // Sequence number
  cid?: number;     // Correlation ID
  ses?: string;     // Session ID
}

Message Types

Value
Name
Direction

1

Ping

Client → Server

2

Pong

Server → Client

3

StatusResponse

Server → Client

5

SubscriptionRequest

Client → Server

6

SubscriptionResponse

Server → Client

7

GasPriceUpdate

Server → Client

8

MarketConfigUpdate

Server → Client

9

MarketStateUpdate

Server → Client

10

MarketFundingUpdate

Server → Client

11

CandlesSnapshot

Server → Client

12

CandlesUpdate

Server → Client

15

L2BookSnapshot

Server → Client

16

L2BookUpdate

Server → Client

17

TradesSnapshot

Server → Client

18

TradesUpdate

Server → Client

19

WalletSnapshot

Server → Client

20

WalletUpdate

Server → Client

21

AccountUpdate

Server → Client

22

OrderRequest

Client → Server

23

OrdersSnapshot

Server → Client

24

OrdersUpdate

Server → Client

25

FillsUpdate

Server → Client

26

PositionsSnapshot

Server → Client

27

PositionsUpdate

Server → Client

28

AccountStatsUpdate

Server → Client

29

ApiKeySignIn

Client → Server

100

Heartbeat

Server → Client

Note: Message types 4, 13, and 14 are reserved and not currently defined.


Market Data WebSocket

The market-data endpoint requires no authentication. Open the connection, then subscribe to one or more streams.

Connecting

Available Streams

Streams are identified by a string of the form <name>@<key>. Chain-scoped streams take the chain ID as the key; market-scoped streams take a market ID.

Stream
Format
Description

heartbeat

heartbeat@<chain_id>

Block sync heartbeat

gas-stats

gas-stats@<chain_id>

Gas price updates

market-config

market-config@<chain_id>

Market configuration

market-state

market-state@<chain_id>

Prices, volume, open interest (OI)

funding

funding@<chain_id>

Funding rate updates

candles

candles@<market_id>*<resolution>

OHLCV (open/high/low/close/volume) candles

order-book

order-book@<market_id>

L2 order book

trades

trades@<market_id>

Recent trades

Chain ID: from PERPL_CHAIN_ID (default 143, Monad Mainnet; 10143 on testnet).

Market IDs (mainnet): BTC=1, MON=10, ETH=20, SOL=31, HYPE=40, ZEC=50. (testnet): BTC=16, ETH=32, SOL=48, MON=64, ZEC=256.

Candle resolutions (seconds): 60, 300, 900, 1800, 3600, 7200, 14400, 28800, 43200, 86400.

Subscribing

Send a SubscriptionRequest (mt: 5) with a subs array. Each entry names a stream and sets subscribe: true to subscribe or subscribe: false to unsubscribe.

Unsubscribing

Send the same SubscriptionRequest frame with subscribe: false:

Subscription Response

The server replies with a SubscriptionResponse (mt: 6). Match the returned sid (subscription ID) against the sid on later update frames to route them to the right handler. status.code === 0 means the subscription succeeded.

Order Book

Snapshot (mt: 15) — the full L2 (aggregated-by-price) book at a block:

Update (mt: 16) — same structure, carrying only changed levels. A level with o: 0 should be removed from your local book.

Trades

Snapshot (mt: 17):

Update (mt: 18) — same structure, containing new trades.

Candles

Snapshot (mt: 11):

Update (mt: 12) — contains up to 2 candles: the previous (now closed) candle and the current (still-updating) candle.

TODO(author): the Candle object's field layout is not defined in the source. Document its fields (e.g. open/high/low/close/volume) once confirmed.

Market State (mt: 9)

Delivered on the market-state@<chain_id> stream. d maps each market ID to its current state.

Heartbeat (mt: 100)

The heartbeat@<chain_id> stream emits a continuously-increasing sequence number and the latest head block. Track sn to detect dropped messages.

TODO(author): the GasPriceUpdate (mt: 7), MarketConfigUpdate (mt: 8), and MarketFundingUpdate (mt: 10) payload shapes are not defined in the source. Document their fields once confirmed.


Trading WebSocket

The trading endpoint delivers your wallet, order, position, and account data and accepts order requests. It requires authentication with an API key.

Authenticating (mt: 29)

API keys are Ed25519 (Edwards-curve Digital Signature Algorithm) key pairs. Create one in the web UI (app.perpl.xyz/apikeys for mainnet, testnet.perpl.xyz/apikeys for testnet) or programmatically (see Authentication). Placing orders requires a trade-scoped key — a read-scoped key still receives snapshots and updates, but its OrderRequest frames are rejected with 403.

Send an ApiKeySignIn frame as the first message after the socket opens. The Ed25519 signature covers the WS canonical string — four fields joined by \n (newline):

Frame shape:

The signature timestamp must be within ±30 seconds of server time, and each nonce is single-use within the validity window. Generate a fresh timestamp and nonce for every sign-in — including on every reconnect.

Initial Snapshots

After successful authentication, the server pushes three snapshots:

  1. WalletSnapshot (mt: 19) — wallet and account balances.

  2. OrdersSnapshot (mt: 23) — open orders.

  3. PositionsSnapshot (mt: 26) — open positions.

The WalletSnapshot carries a sequence number (sn in the message header) that seeds sequence tracking. Store it and validate every subsequent heartbeat against it (see Heartbeat).

Placing Orders (mt: 22)

Idempotency and Request IDs (rq)

rq is an idempotency key scoped per account. The server guarantees at-most-once execution per rq — sending the same rq more than once yields a single execution. It is the API equivalent of a client order ID on centralized exchanges (it applies only to orders sent via the API, not to direct on-chain transactions).

rq must be strictly increasing. The server tracks the last processed value as lfr on the Account object (present in WalletSnapshot mt: 19 and AccountUpdate mt: 21).

  1. On connect, seed a local counter from account.lfr.

  2. For each order: rq = max(localCounter, account.lfr) + 1.

Submitting rq <= lfr fails with sr: 32 (OrderDescIdTooLow).

Note: For smart-contract / SDK users placing non-API orders, rq may be set to any value to identify the order and need not be unique.

Retries and Deduplication

The client is responsible for retries. Multiple status updates can arrive for a single rq; deduplicate them.

Scenario
Action

No status received yet, lb not expired

Retry with the same rq

sr: 32 (OrderDescIdTooLow) received

Retry once with a new rq (common with multiple clients/tabs)

Head block ≥ lb, no status received, no reconnections since posting

Retry with a new rq

Deduplication rules:

  • The first non-failure status (st in 2, 3, 4, 5, 8, 9, 10) is definitive — ignore everything after it, including later failures.

  • If only failures (st: 7) arrive, process the first one only.

  • After retrying with a new rq, ignore late failures from the old rq.

Trigger Orders

  • Trigger orders must set lb: 0 (no expiry block). The server manages their lifecycle from the trigger condition.

  • tp + tpc: the order is not posted until the market last price crosses the trigger price per the condition (GTE = greater-than-or-equal, LTE = less-than-or-equal).

  • tr: links this trigger to another request. When the linked request trades, the trigger activates; when it fails, the trigger is cancelled. If the linked request places an order, the trigger activates when that order fills and cancels when it is cancelled.

  • lp: links the trigger to a position. The trigger is cancelled when the position is closed or inverted.

Order Types (t)

Value
Name

1

OpenLong

2

OpenShort

3

CloseLong

4

CloseShort

5

Cancel

6

IncreasePositionCollateral

7

Change

Order Flags (fl)

Value
Name

0

GoodTillCancel (GTC)

1

PostOnly

2

FillOrKill (FOK)

4

ImmediateOrCancel (IOC)

Example — Open Long

Example — Cancel Order

  • size > 0 — reject zero or negative sizes.

  • leverage within market limits — check MarketConfig.initial_margin (e.g. 1000 = 10% = max 10x).

  • marketId is valid — verify against /api/v1/pub/context markets.

  • price > 0 for limit orders; price = 0 for market (IOC) orders.

  • lb should not exceed head_block_number + market.order_ttl_blocks.

  • The WebSocket is connected — check ws.readyState === WebSocket.OPEN.

Order Updates (mt: 24)

Orders with r: true should be removed from your open-orders view. Order-level status is carried in st (OrderStatus) and reject reasons in sr (OrderStatusReason — see Order Reject Reasons).

OrderStatus (st):

Value
Name

1

Pending

2

Open

3

PartiallyFilled

4

Filled

5

Canceled

6

Expired

7

Failed

8

Untriggered

9

Triggered

10

Executed

Fill Updates (mt: 25)

Each fill carries a LiquiditySide (1 = Maker, 2 = Taker).

Position Updates (mt: 27)

Positions carry a PositionType (1 = Long, 2 = Short).

Account Updates (mt: 21)

AccountEvent entries carry an AccountEventType:

Value
Name

1

Deposit

2

Withdrawal

3

IncreasePositionCollateral

4

Settlement

5

Liquidation

6

TransferToProtocol

7

TransferFromProtocol

8

Funding

9

Deleveraging

10

Unwinding

11

PositionCollateralDecreased

12

LastForwardedDescIdReset

Account Stats (mt: 28)

AccountStatsUpdate (mt: 28) carries per-account trading statistics. The same stats are also delivered inside the WalletSnapshot (mt: 19) via the wallet's sts? field.

TODO(author): the AccountStats field layout is defined in the shared types reference (types-and-errors.md#accountstats), not the WebSocket source. Cross-link or inline once available.

Heartbeat (Trading)

On the trading WebSocket, sequence tracking is initialized from the WalletSnapshot rather than the heartbeat stream:

  1. Initialize lastSn from the sn field of the WalletSnapshot (mt: 19) received after authentication.

  2. Each subsequent heartbeat must satisfy sn === previousSn + 1.

  3. On a sequence gap (missed heartbeat), force reconnect — the gap means messages may have been lost.

Keep-Alive

Send a Ping (mt: 1) about every 30 seconds to keep the connection open:

The server replies with a Pong (mt: 2).


Error Handling and Reconnection

Close Code 3401 — Authentication Failure

Close code 3401 means authentication failed. Reconnect and send a fresh, freshly-signed ApiKeySignIn frame (new timestamp and nonce) as the first message.

Reconnection Strategy

Reconnect with exponential backoff. On every reconnect, re-authenticate as the first frame and re-seed sequence tracking from the new WalletSnapshot.

Order Reject Reasons

Order rejects and position status changes are delivered as sr (OrderStatusReason) on order updates. Common values:

sr

Meaning

1

AmountExceedsAvailableBalance

13

CrossesBook

14

ExceedsLastExecutionBlock

15

ForwardingReverted

32

OrderDescIdTooLow

38

OrderSizeExceedsAvailableSize

53

PerpetualInsolvent

Note: The full OrderStatusReason enum spans values 0–68, and a PositionStatusReason enum (subset 13–22) is also defined. See the shared types reference for the complete lists. No JSON error-envelope schema is specified for WebSocket frames.


Sequence Numbers

  • The heartbeat and gas-stats streams have continuous sequence numbers.

  • Other streams may have gaps (for example, when there is no activity).

  • Track sn to detect missed messages.

  • On a gap, resubscribe (market data) or force reconnect (trading) to obtain a fresh snapshot.

Last updated