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, testnethttps://testnet.perpl.xyz/apikeys); the UI hands you theX-API-Keytoken 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 (tradeimpliesread). Areadkey 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 return404until 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/ed25519fetch 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):
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:
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 window —
X-API-Timestampmust 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 anip_cidrsallow-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:
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.
Wire it up and place an order once the snapshots have arrived:
Order request fields
OrderRequest (mt: 22) key fields:
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
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
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.
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
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
/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
Authentication — the full API-key model and request-signing reference.
Networks & Configuration — every endpoint, contract address, and market ID for both networks.
Last updated