Authentication
Perpl authenticates programmatic clients — bots, trading terminals, and scripts — with API keys. An API key is an Ed25519 key pair (Ed25519 is a public-key signature algorithm). The server only ever stores the public key; the private key never leaves your machine. There is no bearer token and no session cookie to leak: every request is signed with the key's private key.
This page covers how to obtain a key (web UI or programmatic enrollment) and how to sign each REST (Representational State Transfer, i.e. HTTPS) and WebSocket request with it.
The signing scheme includes the network's numeric chain ID, so you must sign with the value for the network you are calling. The two live networks are Mainnet (chain ID 143, default) and Testnet (chain ID 10143). Full endpoint, contract, and market details are on the Networks page — reuse those values rather than hard-coding your own.
Mainnet (default)
143
https://app.perpl.xyz/api
wss://app.perpl.xyz
Testnet
10143
https://testnet.perpl.xyz/api
wss://testnet.perpl.xyz
The REST base URL includes the /api suffix; the WebSocket URL does not.
The snippets below read the key material and network from environment variables:
PERPL_API_URL
REST base URL (e.g. https://app.perpl.xyz/api)
PERPL_WS_URL
WebSocket base URL (e.g. wss://app.perpl.xyz)
PERPL_CHAIN_ID
Chain ID (143 mainnet, 10143 testnet)
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
API keys and scopes
Every key carries a scope, chosen at enrollment. Scope is stored as a 32-bit bitmask (scope_mask). Trade implies read.
scope_mask
Name
Grants
1
read (1 << 0)
Read account, order, position, history, and points/rewards data
2
trade (1 << 1)
Place / cancel / modify orders (implies read)
3
read | trade
Both
Withdrawals and transfers-out are never permitted via an API key, regardless of scope. Those actions require a wallet signature on the Exchange contract directly.
API auth is not the same as an exchange account
This is a common source of confusion. Enrolling and authenticating an API key authorizes API access for your wallet. It does not create an on-chain trading account.
API authentication
An enrolled API key can call authenticated API endpoints
Reading order history, position history, connecting to the trading WebSocket
Exchange account
An on-chain account exists on the Exchange contract with collateral
Placing orders, holding positions, trading
To trade you must additionally create an on-chain account with initial collateral by calling createAccount(uint256 amountCNS) on the Exchange contract. Until that account exists, some authenticated calls return 404. See the Networks page for the Exchange contract address per network, and the account-creation walkthrough for the cast commands.
Creating a key
Web UI
Connect your wallet and create a key at:
Mainnet — app.perpl.xyz/apikeys
Testnet — testnet.perpl.xyz/apikeys
The UI hands you the opaque X-API-Key token and the Ed25519 private key. Store both — the token is not re-derivable.
Listing and revoking keys is done from the web UI (/apikeys), not via the API. A revoked public key cannot be re-enrolled — generate a fresh key pair.
Programmatic enrollment
Third-party integrations can enroll keys directly on behalf of a user's wallet. Enrollment is a one-time, wallet-authorized flow across two endpoints:
POST
/api/v1/api-key/payload
Wallet signature
Get the EIP-712 payload to sign
POST
/api/v1/api-key/enroll
Wallet signature
Enroll the key, receive the token
EIP-712 is the Ethereum standard for signing typed structured data. The flow is:
Request the enrollment payload
POST /api/v1/api-key/payload with an ApiKeyPayloadRequest body:
It returns an ApiKeyPayloadResponse:
To enroll a key for a delegated account (an operator acting for another profile), set target_profile to the delegated account address. address stays the signing wallet (owner or operator); the server resolves the principal and validates the delegation on-chain at enrollment.
Sign and enroll
Enrollment requires two signatures over the returned typed_data:
Wallet signature — the wallet's secp256k1 EIP-712 signature (secp256k1 is the elliptic curve used by Ethereum wallets). Proves the user owns or operates the account.
Proof-of-possession — an Ed25519 signature by the API private key over the EIP-712 digest
keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message)). Proves you hold the private key for the public key being enrolled.
POST /api/v1/api-key/enroll with an ApiKeyEnrollRequest body:
The response is an ApiKeyInfo. Its api_key field is the opaque X-API-Key token — store it, it is not re-derivable.
Enroll status codes:
404
Target profile not found
409
Public key already registered (revoked keys can't be re-enrolled — use a fresh key pair)
423
Per-profile key limit reached (max 16 active keys)
Signing REST requests
Sign a canonical string with the key's private key and send four headers. No cookies, no bearer token.
The canonical string is these six fields joined by \n (a single newline between each):
The signature is base64url(ed25519_sign(privateKey, canonical)) — base64url encoded, no padding — sent in the X-API-Signature header. (SHA-256 is a cryptographic hash; base64url is the URL-safe base64 alphabet.)
Four headers are required on every authenticated request:
X-API-Key
The opaque token from enrollment
X-API-Timestamp
The same timestamp_ms used in the canonical string
X-API-Nonce
The same nonce used in the canonical string
X-API-Signature
base64url(ed25519 signature)
The request-target must match byte-for-byte what the server receives — include the full query string (?count=100&page=...) exactly as sent, in the same order. Any mismatch changes the canonical string and the signature will be rejected.
Worked example
Suppose you are calling Mainnet (chain_id = 143) with a GET on /v1/trading/fills?count=1, an empty body, at timestamp_ms = 1751932800000, with nonce = 9Nq0Yp3kZ2c1aVb7 (base64url, no padding). The SHA-256 of the empty body is the well-known constant e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
The six fields assemble into this exact byte sequence (each line separated by a single \n):
You then sign that exact string with the Ed25519 private key and base64url-encode (no padding) the result to produce X-API-Signature. The request carries the four headers:
X-API-Timestamp and X-API-Nonce must be the same values you placed into the canonical string. Re-generate both (and re-sign) for every request.
WebSocket authentication
The trading WebSocket lives at /ws/v1/trading. Authenticate by sending an ApiKeySignIn frame (message type mt: 29) as the first message after the socket opens.
The signature covers a WebSocket canonical string — four fields joined by \n:
A trade-scoped key may place orders over the socket. A read-scoped key still receives snapshots and updates, but OrderRequest frames are rejected with status 403.
The market-data WebSocket (/ws/v1/market-data) requires no authentication — connect and subscribe directly.
Signature validity
Timestamp window
X-API-Timestamp must be within ±30 seconds of server time. Keep the client clock in sync (NTP).
Nonce
Single-use within the validity window — generate a fresh random nonce per request. Replays are rejected.
Expiry
Requests are rejected once the key is past its expires_at.
IP allow-list
When an ip_cidrs allow-list is set (max 4 CIDRs), requests from an IP outside it are rejected.
Errors
HTTP status codes
200
Success
—
400
Bad Request
Check the request shape / body
401
Unauthorized — missing/invalid headers, bad or stale signature, replayed nonce, revoked/expired key, or IP not allowed
Re-sign with a fresh timestamp + nonce; check the clock, key status, and source IP
403
Forbidden — scope insufficient (e.g. a read key attempting to trade)
Enroll a trade-scoped key
404
Not Found — including when no on-chain exchange account exists yet
Create an exchange account with createAccount()
429
Too Many Requests
Back off (see rate limits)
500
Internal Server Error
Retry with backoff
WebSocket close code
3401
Authentication failure
Re-send a fresh signed ApiKeySignIn (mt: 29) frame and reconnect
Next steps
Networks — endpoints, chain IDs, contract and collateral addresses, market IDs.
REST Endpoints — the full list of HTTP endpoints and which require a signed request.
WebSocket — real-time streams, subscription frames, and order placement over the trading socket.
Last updated