Python
This guide ports the Perpl API — the REST (Representational State Transfer, i.e. HTTPS) endpoints and the WebSocket (WSS) streams — to idiomatic Python. Every request is signed with an API key, so the bulk of this page is a small, reusable signing layer plus copy-pasteable examples for the public data, authenticated history, and trading flows.
The signing scheme, endpoints, and message types are identical across languages; only the tooling differs. Where a detail is specific to Python (a library choice, a request-encoding gotcha) it is called out explicitly.
Note: Perpl authenticates programmatic clients with API keys. An API key is an Ed25519 key pair (Ed25519 is a public-key signature algorithm). The server stores only the public key; the private key never leaves your machine. There is no bearer token or session cookie — every request is signed with the private key. Withdrawals and transfers-out are never permitted via an API key, regardless of scope.
Prerequisites
Install three third-party packages:
requests
REST calls over HTTPS
websocket-client
WebSocket streams (imported as websocket)
pynacl
Ed25519 signing
pip install requests websocket-client pynaclNote:
pynaclis used throughout this page for Ed25519. If you already depend on thecryptographypackage you can swap it in — both accept a 32-byte private-key seed. See Loading the key for the one-line difference.
You also need an enrolled API key. Create one in the web UI (connect your wallet at https://app.perpl.xyz/apikeys for mainnet or https://testnet.perpl.xyz/apikeys for testnet); the UI hands you the X-API-Key token and the Ed25519 private key. Programmatic enrollment is also possible — see Enrolling a key programmatically.
Note: An API key authorizes API access. It does not create an on-chain trading account. Trading also requires an exchange account created on-chain with initial collateral; some authenticated calls return
404until that account exists.
Configuration
The examples read the network and key material from environment variables, matching the reference clients. Full endpoint, contract, and market values for both networks are on the Networks page — reuse those rather than hard-coding your own.
PERPL_API_URL
REST base URL (includes /api)
https://app.perpl.xyz/api
PERPL_WS_URL
WebSocket base URL (no /api)
wss://app.perpl.xyz
PERPL_CHAIN_ID
Chain ID (143 mainnet, 10143 testnet)
143
PERPL_API_KEY
The opaque X-API-Key token from enrollment
—
PERPL_API_KEY_SECRET
The Ed25519 private key, hex of the 32-byte key
—
Note: The chain ID is part of the signed canonical string, so you must sign with the value for the network you are calling. A signature built for chain
143is rejected on testnet (chain10143).
The signing primitive
Both REST and WebSocket authentication sign a canonical string — a fixed set of fields joined by newline (\n) — with the Ed25519 private key, then base64url-encode the 64-byte signature without padding. These helpers are shared by every signed call.
Loading the key
The private key arrives as hex of the 32-byte seed (with or without a 0x prefix):
Note: With the
cryptographypackage instead ofpynacl, this becomesEd25519PrivateKey.from_private_bytes(seed), and signing iskey.sign(message)(which already returns the 64-byte signature directly).
Encoding helpers
Signing REST requests
A REST canonical string is six fields joined by \n:
Send four headers alongside the request:
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)
Note: The
request-targetmust match byte-for-byte what the server receives. In Python this means you must build the query string yourself and sign that exact string — do not handrequestsaparams={...}dict, becauserequestsmay re-encode or reorder parameters, breaking the signature. Build the target withurllib.parse.urlencodeonce, sign it, and request that same URL. The pagination helper below shows the pattern.
Public data (no auth)
Public endpoints require no signature. Fetch the global context (chain, instances, tokens, market configs) and candles with plain requests calls.
Context and market scaling
Prices and sizes on the wire are scaled integers. Divide by 10 ** price_decimals / 10 ** size_decimals (read from each market's config in the context) to get human-readable values; leverage is in hundredths.
Scaling round-trips, using BTC on mainnet (price_decimals = 1, size_decimals = 5):
Candles
The candles endpoint takes a market_id, a resolution in seconds, and a from-to millisecond range. A maximum of 1024 candles are returned per request. Supported resolutions (seconds): 60, 300, 900, 1800, 3600, 7200, 14400, 28800, 43200, 86400.
Authenticated history
The trading-history endpoints are all GET, signed with the four X-API-* headers, and paginated. Response shape is {"d": [...newest to oldest...], "np": "<next cursor>"}. Pass count (default 50, max 100) and page (the np cursor from the previous response). Server-side filtering by market or date is not supported — filter client-side.
GET /v1/trading/fills
Order fill history
GET /v1/trading/order-history
Historical order events
GET /v1/trading/position-history
Position history
GET /v1/trading/account-history
Account events (deposits, settlements, funding, …)
GET /v1/profile/ref-code
Your referral code (404 + empty if none)
AccountEvent.et (event type) values on account-history: 1 Deposit, 2 Withdrawal, 3 IncreasePositionCollateral, 4 Settlement, 5 Liquidation, 6 TransferToProtocol, 7 TransferFromProtocol, 8 Funding, 9 Deleveraging, 10 Unwinding, 11 PositionCollateralDecreased, 12 LastForwardedDescIdReset.
Rate limits and error handling
Limits are approximate; back off on HTTP 429.
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
REST status codes and what to do:
400
Bad request
Fix the request shape
401
Bad/stale signature, replayed nonce, revoked/expired key, or IP not in the allow-list
Re-sign with a fresh timestamp + nonce; check the system clock, key status, and source IP
403
Scope insufficient (e.g. a read key placing an order)
Use a trade-scoped key
404
Not found (e.g. no exchange account, or no referral code)
Verify the on-chain account exists
429
Too many requests
Exponential backoff (1s / 2s / 4s)
500
Internal server error
Retry with backoff
Note: The signature timestamp must be within ±30 seconds of server time, and each
nonceis single-use within that window. A clock that drifts more than 30 seconds produces401on every request — keep the client clock in sync (NTP).
WebSocket: market data
The /ws/v1/market-data endpoint needs no authentication. Subscribe with a SubscriptionRequest (mt: 5) carrying a subs list of {stream, subscribe}. Available streams: heartbeat@<chain_id>, gas-stats@<chain_id>, market-config@<chain_id>, market-state@<chain_id>, funding@<chain_id>, candles@<market_id>*<resolution>, order-book@<market_id>, trades@<market_id>.
Every message carries a header: mt (message type), optional sid (subscription id), sn (sequence), cid (correlation id), ses (session id).
Relevant server → client message types: 9 MarketStateUpdate, 10 MarketFundingUpdate, 11/12 Candles snapshot/update, 15/16 L2 (level-2) book snapshot/update, 17/18 Trades snapshot/update, 100 Heartbeat (carries sn and h = latest head block).
WebSocket: trading
The /ws/v1/trading endpoint requires authentication.
The mt: 29 signature covers a four-field canonical string joined by \n: <chain_id>, the literal tag trading-ws-signin, <timestamp_ms>, <nonce>.
Note: A
read-scoped key connects and receives all snapshots and updates, but itsOrderRequestframes are rejected. Placing orders needs atrade-scoped key. WebSocket close code 3401 means authentication failed — reconnect and re-send a fresh signedmt: 29frame.
The client below authenticates, tracks the head block and the order sequence, keeps the connection alive on a background thread, and exposes order-placement methods.
Request IDs, idempotency, and retries
rq (Request ID) is an idempotency key scoped per account; the server guarantees at-most-once execution per rq. It must be strictly increasing. The server tracks the last processed value as lfr (last forwarded request id), delivered on WalletSnapshot (mt: 19) and AccountUpdate (mt: 21).
On connect, seed a local counter from
account.lfr.For each order,
rq = max(local_counter, account.lfr) + 1.Submitting
rq <= lfrfails withsr: 32(OrderDescIdTooLow).
No status received yet, lb (last valid block) not expired
Retry with the same rq
sr: 32 (OrderDescIdTooLow)
Retry once with a new rq (common with multiple clients/tabs)
Head block ≥ lb, no status received, and no reconnections since posting
Retry with a new rq
Multiple updates can arrive for one rq. Deduplicate on the client: take the first non-failure status (st in 2,3,4,5,8,9,10) as definitive and ignore everything after it; if only failures (st: 7) arrive, process the first one only.
Order fields and enums
OrderRequest (mt: 22) fields:
rq
Request ID (idempotency key, strictly increasing)
mkt
Market ID
acc
Account ID
oid
Order ID (for modify / cancel)
t
Order type (see below)
p
Limit price, scaled (0 = market)
s
Size, scaled by size_decimals
a
Amount (for collateral increase)
ms
Max market-order slippage, basis points (bps)
tif
Time-in-force (also defined; lb is the operative field for order validity/expiry)
fl
Flags (see below)
tp / tpc
Trigger price and condition
tr
Linked trigger request ID
lp
Linked position ID
lv
Leverage in hundredths (1000 = 10x)
lb
Last execution block
Order type (t)
Flag (fl)
Trigger condition (tpc)
1
OpenLong
0
GoodTillCancel (GTC)
1
GTELast
2
OpenShort
1
PostOnly
2
LTELast
3
CloseLong
2
FillOrKill (FOK)
3
GTEMark
4
CloseShort
4
ImmediateOrCancel (IOC)
4
LTEMark
5
Cancel
6
IncreasePositionCollateral
7
Change
Note: Trigger orders (a take-profit / stop-loss, TP/SL) must set
lb: 0— they have no expiry block, and the server manages their lifecycle.tp+tpcfire when the market last/mark price crosses the trigger;trlinks the trigger to another request;lplinks it to a position (cancelled when the position closes or inverts).
Order-reject reasons are delivered as sr (OrderStatusReason) on order updates — a 0–68 enum. Common values: 1 AmountExceedsAvailableBalance, 13 CrossesBook, 14 ExceedsLastExecutionBlock, 15 ForwardingReverted, 32 OrderDescIdTooLow, 38 OrderSizeExceedsAvailableSize, 53 PerpetualInsolvent.
Enrolling a key programmatically
Most integrations create keys in the web UI. Programmatic enrollment is a one-time, wallet-authorized flow over two endpoints:
Key facts for the flow:
Scope bitmask (
scope_mask, uint32):1= read (1 << 0),2= trade (1 << 1, implies read),3= both.Origin whitelisting: both enroll endpoints are CORS-enabled and the request
Originmust be pre-whitelisted by Perpl; a non-browser client must setOriginexplicitly.Delegated accounts: set
target_profileto the delegated account address;addressstays the signing wallet (owner/operator). Delegation is validated on-chain at enrollment.Enroll error codes:
404target profile not found;409public key already registered (revoked keys cannot be re-enrolled — use a fresh keypair);423per-profile key limit reached (max 16 active keys).Listing and revoking keys is done in the web UI (
/apikeys), not via the API.
TODO(author): verify a full Python enrollment example end-to-end. The source docs provide only a JavaScript reference (
examples/js/enroll_api_key.js); a Python port needs a secp256k1 EIP-712 signer (e.g.eth-account) plus a hand-built proof-of-possession digest (keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))). The exact request/response JSON field names forPOST /api/v1/api-key/payloadandPOST /api/v1/api-key/enrollare not enumerated in the API reference and should be confirmed againstexamples/js/enroll_api_key.jsbefore publishing runnable code.
Next steps
Networks & Configuration — every endpoint, contract address, chain ID, and market ID for both networks.
REST API — full endpoint reference and response types.
WebSocket API — all message types, streams, and trading-flow semantics.
Authentication — the signing scheme in full, including scopes and validity rules.
Last updated