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

REST

The Perpl REST (Representational State Transfer) API is served over HTTPS and covers public market data, account and trading history, and API-key enrollment. Streaming data (order books, live prices, order/position updates, order placement) is handled by the WebSocket API, not REST.

All response types are generated from Go structs and shipped as TypeScript interfaces (via tygo), so the field names shown below match the wire format exactly.

Base URL

The REST base URL includes the /api suffix. Choose the base URL for your target network:

Network
REST base URL
Chain ID

Mainnet (default)

https://app.perpl.xyz/api

143

Testnet

https://testnet.perpl.xyz/api

10143

The examples on this page read the base URL from the PERPL_API_URL environment variable, falling back to the mainnet default:

export PERPL_API_URL="https://app.perpl.xyz/api"    # mainnet
# export PERPL_API_URL="https://testnet.perpl.xyz/api"  # testnet

Market IDs differ per network. Mainnet: BTC=1, MON=10, ETH=20, SOL=31, HYPE=40, ZEC=50. Testnet: BTC=16, ETH=32, SOL=48, MON=64, ZEC=256. For the full list of network values (RPC URLs, contract addresses, collateral token) see Networks.

Authentication overview

Authentication uses API keys, which are Ed25519 key pairs enrolled once with a wallet signature. The server stores only your public key; the private key never leaves your client. There is no bearer token and no session cookie — every authenticated request is signed with four headers:

Header
Value

X-API-Key

The opaque token returned at enrollment

X-API-Timestamp

Request timestamp in milliseconds (must be within ±30 seconds of server time)

X-API-Nonce

Client-random, single-use base64url value (no padding)

X-API-Signature

base64url(ed25519_sign(privateKey, canonicalString)), no padding

Each endpoint below is labelled with one of three authentication requirements:

  • None — no signature needed.

  • Optional — works unauthenticated; providing an API-key signature personalizes the response.

  • API key — an API-key signature is required.

A key also carries a scope (read, trade, or both). REST history and profile reads require read; trade is used for order placement over WebSocket. Withdrawals and transfers-out are never permitted via API key, under any scope.

Enrolling an API key only authorizes API access — it does not create an exchange account. Trading additionally requires an on-chain account created via createAccount(uint256 amountCNS) on the Exchange contract. Endpoints that need an on-chain account may return 404 if none exists.

For the full canonical-string format and a signing helper, see Authentication. To obtain a key, see Authentication → Creating a key.


Public endpoints

GET /api/v1/pub/context

Returns global protocol configuration: chain, protocol instances, tokens, and markets.

  • Authentication: Optional (a signature personalizes the response)

Response:

Each ProtocolInstance carries operational limits such as min_account_open_amount, min_deposit_amount, min_withdraw_amount, and max_account_trigger_orders. Each Market carries price_decimals and size_decimals, which you use to scale integer prices and sizes into human-readable values (see Types).

Example:


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

Returns OHLCV (open-high-low-close-volume) candlestick data.

  • Authentication: None

URL parameters:

Parameter
Type
Description

market_id

number

Market ID (e.g. 1 for BTC on mainnet)

resolution

number

Candle resolution in seconds (see supported values)

from

number

Start timestamp (ms)

to

number

End timestamp (ms)

Limits: A maximum of 1024 candles per request.

Supported resolutions (seconds): 60 (1m), 300 (5m), 900 (15m), 1800 (30m), 3600 (1h), 7200 (2h), 14400 (4h), 28800 (8h), 43200 (12h), 86400 (1d).

Response:

Example:


GET /api/v1/profile/announcements

Returns active announcements.

  • Authentication: Optional (works unauthenticated for the public audience; a signature personalizes the returned announcements)

Response:

Example:


API-key enrollment endpoints

Enrollment is a one-time, wallet-authorized flow that turns a locally generated Ed25519 key pair into an X-API-Key token. Both endpoints below are authorized by a wallet signature, not an API-key signature, and are CORS (cross-origin resource sharing) enabled — the request Origin must be pre-whitelisted by Perpl.

For the full step-by-step flow (keypair generation, EIP-712 signing, proof-of-possession), see Authentication → Programmatic enrollment.

POST /api/v1/api-key/payload

Returns the EIP-712 typed data to sign for enrollment, plus an opaque mac that you echo back on enroll.

  • Authentication: Wallet signature

Purpose: Obtain the typed_data + mac used in the next step.

POST /api/v1/api-key/enroll

Enrolls the public key and returns ApiKeyInfo. The api_key.api_key field is the opaque X-API-Key token.

  • Authentication: Wallet signature

Request (echo typed_data + mac from the payload step, plus two signatures):

  • signature — wallet secp256k1 EIP-712 signature (proves account ownership)

  • pop_signature — Ed25519 proof-of-possession over the enrollment digest

Store the returned X-API-Key token immediately — it is not re-derivable. Listing and revoking keys is done in the web UI (/apikeys), not via the API. A revoked public key cannot be re-enrolled; use a fresh keypair.

Enroll status codes:

Code
Meaning

404

Target profile not found

409

Public key already registered (revoked keys are not re-enrollable)

423

Per-profile key limit reached (maximum 16 active keys)


Profile endpoints

GET /api/v1/profile/ref-code

Returns your current referral code.

  • Authentication: API key

Response:

Returns 404 with an empty code if no referral code is assigned.

Example ($SIG, $TS, $NONCE are the signed values — see Authentication):


Trading history endpoints

All trading history endpoints require an API-key signature and support pagination. The response is always a page object:

Pagination query parameters:

Parameter
Type
Default
Description

page

string

Cursor from the previous response's np

count

number

50

Items per page (maximum 100)

Server-side filtering by market ID or date range is not currently supported. Filter results client-side if needed.

GET /api/v1/trading/account-history

Returns account events (deposits, withdrawals, settlements, funding, and more).

  • Authentication: API key

Response:

Account event types (et):

Value
Name

0

Unspecified

1

Deposit

2

Withdrawal

3

IncreasePositionCollateral

4

Settlement

5

Liquidation

6

TransferToProtocol

7

TransferFromProtocol

8

Funding

9

Deleveraging

10

Unwinding

11

PositionCollateralDecreased

12

LastForwardedDescIdReset


GET /api/v1/trading/fills

Returns order fill history.

  • Authentication: API key

Response:


GET /api/v1/trading/order-history

Returns historical order events.

  • Authentication: API key

Response:

See Types for the Order structure.


GET /api/v1/trading/position-history

Returns position history.

  • Authentication: API key

Response:

See Types for the Position structure.


Pagination example

Each request is signed with the API-key headers. signedRequest(method, target, body) is the helper defined in Authentication — note that the request-target (path + query string) must be signed exactly as sent.


Rate limits

Rate limits are approximate. Monitor for HTTP 429 responses and back off.

Type
Limit
Applies to

REST public

~100 req/min

/api/v1/pub/*, market data

REST authenticated

~60 req/min

profile, trading history

On a 429 Too Many Requests, retry with exponential backoff (for example 1s, then 2s, then 4s).

Errors

HTTP status codes:

Code
Meaning

200

Success

400

Bad Request

401

Unauthorized — bad or stale signature, replayed nonce, or a revoked/expired key

403

Forbidden — insufficient scope (for example a read key attempting a trade action)

404

Not Found — including no on-chain account for the caller

429

Too Many Requests

500

Internal Server Error

A request is rejected with 401 if the timestamp is outside the ±30-second window, the nonce has already been used within the validity window, the key is past its expires_at, or the caller IP is not in the key's ip_cidrs allow-list (CIDR = Classless Inter-Domain Routing; maximum 4 CIDRs) when one is set.

No JSON error-envelope schema is defined in the source documentation. Inspect the HTTP status code to classify failures.

Last updated