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

TypeScript

This guide shows how to talk to the Perpl API directly from TypeScript — no SDK. You will configure a client from environment variables, sign requests with an Ed25519 (Edwards-curve Digital Signature Algorithm) key, make authenticated REST (Representational State Transfer) calls, subscribe to a WebSocket stream, and place and cancel an order.

Every snippet below is copy-pasteable. Fill in an enrolled API key and a target network, and the code runs as-is.

What you need first

Before writing code, make sure you have:

  • An enrolled API key — an Ed25519 key pair. The server stores only the public key; the private key never leaves your machine, and every request is signed with it. There is no bearer token or session cookie. Create a key by connecting your wallet in the web UI (mainnet https://app.perpl.xyz/apikeys, testnet https://testnet.perpl.xyz/apikeys); the UI hands you the X-API-Key token and the private key. See Authentication for the full model.

  • A key scope that matches your task — a key carries a scope of read, trade, or both (trade implies read). A read key can fetch data and subscribe to streams but cannot place orders. Withdrawals and transfers-out are never permitted via an API key, at any scope.

  • An on-chain exchange account — API authentication is separate from having an account to trade with. Authenticating a key only authorizes API access; trading also requires an on-chain account created with collateral via createAccount(uint256) on the Exchange contract. A signed request will succeed at the API layer but some calls return 404 until that account exists.

Prices, sizes, and collateral amounts are scaled integers, and leverage is expressed in hundredths (1000 = 10x). See Scaling helpers below. Never send a human-readable float where the API expects a scaled integer.

Install dependencies

The only third-party dependency is @noble/ed25519 for signing. Everything else (fetch, WebSocket, and the Node crypto module) is provided by the runtime.

npm install @noble/ed25519

fetch and WebSocket are global in browsers and in Node 22+. On older Node versions, provide a WebSocket implementation (for example the ws package) before running the WebSocket snippets. createHash and randomBytes come from Node's built-in crypto module.

Configure the client

All endpoints and chain settings are read from environment variables, so switching between mainnet and testnet is a matter of swapping values — no code changes. Create a .env (or export the variables in your shell):

Mainnet:

Testnet:

The REST base URL includes the /api suffix; the WebSocket base URL does not. Connect WebSockets to ${PERPL_WS_URL}/ws/v1/market-data and ${PERPL_WS_URL}/ws/v1/trading.

Load the configuration into a small module you can import everywhere:

Market IDs are network-specific and can change as markets are added or delisted. Fetch the authoritative list at runtime from GET /api/v1/pub/context (see Fetch market configuration) rather than relying on the hard-coded table above.

Sign REST requests

Every REST call is signed. The signature covers a canonical string — six fields joined by \n (newline):

#
Field
Value

1

<chain_id>

e.g. 143

2

<HTTP_METHOD>

GET, POST, …

3

<request-target>

path + query string exactly as sent, e.g. /v1/trading/fills?count=100

4

<timestamp_ms>

Unix epoch milliseconds, decimal

5

<nonce>

client-random, base64url, no padding

6

<sha256(body) hex>

hex SHA-256 of the raw body ("" for an empty body)

The signature is base64url(ed25519_sign(privateKey, canonical)) (base64url, no padding), sent alongside three more headers:

Header
Value

X-API-Key

the opaque token from enrollment

X-API-Timestamp

the timestamp_ms used in the canonical string

X-API-Nonce

the nonce used in the canonical string

X-API-Signature

base64url(ed25519 signature)

The signedFetch helper builds the canonical string, signs it, and attaches all four headers:

The request-target must match byte-for-byte what the server receives. Include the query string (?count=100&page=...) exactly as it appears in the URL you fetch — build it once and reuse the same string for both the signature and the request.

Signature validity rules:

  • Timestamp windowX-API-Timestamp must be within ±30 seconds of server time. Keep the client clock in sync.

  • Nonce — single-use within the validity window. Generate a fresh random nonce per request; replays are rejected.

  • Expiry / IP — requests are rejected once the key is past its expires_at, or when an ip_cidrs allow-list is set (max 4 CIDRs) and the caller's IP is not covered.

Authenticated REST calls

Fetch market configuration

The public context endpoint needs no authentication and returns the live chain, token, and market configuration. Read it once at startup to discover the current market set and the scaling decimals for each market.

getContext() uses a plain fetch because /v1/pub/context is public. A signed request personalizes the response but is optional. Each MarketConfig carries price_decimals and size_decimals used for scaling (see Scaling helpers).

Fetch trading history with pagination

Trading-history endpoints (fills, order-history, position-history, account-history) require a signed request. They page with a cursor: pass count (default 50, max 100) and page (the cursor returned as np in the previous response). The response shape is { d: T[], np: string }, where d is newest-to-oldest and np is the next cursor (absent or empty when there are no more pages).

The same pattern works for position history — only the path and page size change:

History endpoints do not support server-side filtering by market or date — filter the returned rows client-side.

Fetch candles

Candle (OHLCV — open, high, low, close, volume) data is public and returns raw scaled prices. Divide by the market's price scale to get human-readable values. A single request returns at most 1024 candles.

resolution is in seconds. Supported values: 60, 300, 900, 1800, 3600, 7200, 14400, 28800, 43200, 86400.

Subscribe to a market-data WebSocket

The market-data WebSocket (/ws/v1/market-data) requires no authentication. Subscribe with a SubscriptionRequest frame (mt: 5) listing one or more streams. This example maintains a live L2 (Level 2, aggregated) order book: apply the snapshot (mt: 15), then apply each incremental update (mt: 16). A level with zero orders (o === 0) has been removed.

Other public streams follow the same mt: 5 subscription shape — swap the stream value:

Stream
Purpose

heartbeat@<chain_id>

Block-sync heartbeat

gas-stats@<chain_id>

Gas price

market-config@<chain_id>

Market configuration

market-state@<chain_id>

Prices, volume, open interest

funding@<chain_id>

Funding rate

candles@<market_id>*<resolution>

OHLCV

order-book@<market_id>

L2 book

trades@<market_id>

Recent trades

Place and cancel orders over the trading WebSocket

The trading WebSocket (/ws/v1/trading) is authenticated.

1

Open the socket and sign in

Open the socket and send a signed ApiKeySignIn frame (mt: 29) as the first message. Its signature covers a four-field canonical string — <chain_id>, the literal tag trading-ws-signin, <timestamp_ms>, <nonce> — joined by \n.

2

Receive the initial snapshots

Receive the initial snapshots: WalletSnapshot (mt: 19), OrdersSnapshot (mt: 23), PositionsSnapshot (mt: 26). Read your account ID from the wallet snapshot.

3

Seed sequence tracking

Seed sequence tracking from the WalletSnapshot sn field. Every Heartbeat (mt: 100) must carry sn + 1; a gap means you missed a message — force a reconnect.

4

Keep the socket alive

Keep the socket alive by sending a Ping (mt: 1) roughly every 30 seconds.

5

Place orders

Place orders with OrderRequest frames (mt: 22).

Wire it up and place an order once the snapshots have arrived:

Order request fields

OrderRequest (mt: 22) key fields:

Field
Meaning

rq

Request ID — idempotency key, at-most-once per account, strictly increasing (see the note below)

mkt

Market ID

acc

Account ID (from WalletSnapshot)

oid

Order ID — required for Cancel

t

Order type (see enum below)

p

Price, scaled (0 = market order)

s

Size, scaled

ms

Max market slippage, basis points (optional)

lb

Last valid block — the order expires after this block; triggers must set lb: 0

fl

Order flags (see enum below)

lp

Position ID (for closing / trigger fields)

lv

Leverage in hundredths (1000 = 10x)

rq is an idempotency key and must be strictly increasing per account. The client above seeds it from Date.now(), which is simple but not restart-safe. The robust seed is the account's last-forwarded request ID (lfr): compute rq = max(localCounter, lfr) + 1. An rq at or below the account's last value fails with order-reject reason sr: 32 (OrderDescIdTooLow).

Order enums

Enum
Values

OrderType (t)

1 OpenLong, 2 OpenShort, 3 CloseLong, 4 CloseShort, 5 Cancel, 6 IncreasePositionCollateral, 7 Change

OrderFlags (fl)

0 GTC (good-till-canceled), 1 PostOnly, 2 FillOrKill, 4 ImmediateOrCancel

TriggerPriceCondition

1 GTELast, 2 LTELast, 3 GTEMark, 4 LTEMark

OrderStatus (st)

1 Pending, 2 Open, 3 PartiallyFilled, 4 Filled, 5 Canceled, 6 Expired, 7 Failed, 8 Untriggered, 9 Triggered, 10 Executed

A read-scoped key may connect and receive snapshots and updates over the trading WebSocket, but OrderRequest frames are rejected — use a trade-scoped key to place orders.

Scaling helpers

Prices and sizes are integers scaled by the market's price_decimals / size_decimals (read from MarketConfig via /pub/context). Leverage is stored in hundredths. Convert at the client boundary so the rest of your code works in human units.

Collateral amounts settle in a 6-decimal token — divide a raw on-chain integer by 1_000_000 for a USD figure. Fees are expressed in Micros (10⁻⁶; a negative value is a rebate) and monetary Amount fields are decimal strings. See Networks & Configuration.

Error handling and rate limits

HTTP status codes

Status
Meaning
Action

200

Success

400

Bad Request

Check request shape and parameters

401

Unauthorized — bad/stale signature, replayed nonce, revoked/expired key, IP not allowed

Re-sign with a fresh timestamp + nonce; check clock, key status, source IP

403

Forbidden — scope insufficient (e.g. read key placing an order)

Use a trade-scoped key

404

Not Found — including "no on-chain account yet"

Create an exchange account, or check the path

429

Too Many Requests

Back off and retry (see below)

500

Internal Server Error

Retry with backoff

Rate limits

Limits are approximate; watch for HTTP 429 and back off.

Type
Limit

REST public (/v1/pub/*, market data)

~100 req/min

REST authenticated (profile, trading history)

~60 req/min

WebSocket messages

~50 msg/sec per connection

WebSocket connections

~5 per IP

Retrying and reconnecting

Retry 429s with exponential backoff (1s / 2s / 4s …), and reconnect the WebSocket with a backoff schedule. A close code of 3401 means WebSocket authentication failed — reconnect and re-send a fresh signed mt: 29 frame with a new timestamp and nonce.

Order rejections do not arrive as HTTP errors — they come back on order updates as an sr (OrderStatusReason) code. Common values include 1 AmountExceedsAvailableBalance, 13 CrossesBook, 14 ExceedsLastExecutionBlock, 15 ForwardingReverted, 32 OrderDescIdTooLow, 38 OrderSizeExceedsAvailableSize, 53 PerpetualInsolvent. Inspect the sr field on OrdersUpdate (mt: 24) frames to see why an order did not rest or fill.

Reference

REST endpoints

Method
Path
Auth
Purpose

GET

/api/v1/pub/context

Optional

Chain, instances, tokens, markets config

GET

/api/v1/market-data/:market_id/candles/:resolution/:from-:to

None

OHLCV candles (max 1024/req)

GET

/api/v1/profile/announcements

Optional

Announcements

GET

/api/v1/profile/ref-code

API key

Your referral code

GET

/api/v1/trading/account-history

API key

Account events (deposits, settlements, funding, …)

GET

/api/v1/trading/fills

API key

Order fill history

GET

/api/v1/trading/order-history

API key

Historical order events

GET

/api/v1/trading/position-history

API key

Position history

POST

/api/v1/api-key/payload

Wallet signature

Get EIP-712 payload to sign for enrollment

POST

/api/v1/api-key/enroll

Wallet signature

Enroll a key, receive the X-API-Key token

WebSocket message types

Endpoint
Auth

/ws/v1/market-data

None

/ws/v1/trading

API key (mt: 29 sign-in)

Client-to-server frames: 1 Ping, 5 SubscriptionRequest, 22 OrderRequest, 29 ApiKeySignIn. Server-to-client frames include 3 StatusResponse, 6 SubscriptionResponse, 9 MarketStateUpdate, 15/16 L2Book snapshot/update, 17/18 Trades snapshot/update, 19/20 Wallet snapshot/update, 21 AccountUpdate, 23/24 Orders snapshot/update, 25 FillsUpdate, 26/27 Positions snapshot/update, 28 AccountStatsUpdate, 100 Heartbeat.

Next steps

Last updated