Generate Request Signature
Perpo uses the ed25519 elliptic curve standard for request authentication via signature verification.
Perpo Account ID
Register your account and obtain your account ID. The registration steps are provided here . Add your account ID to the request header as perpo-account-id.
Perpo Key
Add your Perpo public key to the request header as perpo-key. To generate and add a new Perpo key, see the documentation .
Timestamp
Take the current timestamp in milliseconds and add it as perpo-timestamp to the request header.
Normalize request content
Normalize the message to a string by concatenating the following:
Current timestamp in milliseconds, e.g. 1649920583000
HTTP method in uppercase, e.g. POST
Request path including query parameters (without base URL), e.g. /v1/orders?symbol=PERP_BTC_USDC
(Optional) If the request has a body, JSON stringify it and append
Example result: 1649920583000POST/v1/order{"symbol": "PERP_ETH_USDC", "order_type": "LIMIT", "order_price": 1521.03, "order_quantity": 2.11, "side": "BUY"}
Generate signature
Sign the normalized content using the ed25519 algorithm, encode the signature in base64 url-safe format, and add the result to the request header as perpo-signature.
Content type
Set the Content-Type header:
GET and DELETE: application/x-www-form-urlencoded
POST and PUT: application/json
Send the request
The final request should have the following headers: Header Description Content-TypeRequest content type perpo-account-idYour Perpo account ID perpo-keyYour Perpo public key perpo-signatureed25519 signature (base64url) perpo-timestampRequest timestamp (ms)
The Perpo key should be used without the ed25519: prefix when used in code samples below.
Full Example
Java
Python
TypeScript
Shell
AuthenticationExample.java
Signer.java
import io.github.cdimascio.dotenv.Dotenv;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.bitcoinj.base.Base58;
import org.json.JSONObject;
public class AuthenticationExample {
public static void main ( String [] args ) throws Exception {
String baseUrl = "https://testnet-api.perpo.trade" ;
String orderlyAccountId = "<perpo-account-id>" ;
Dotenv dotenv = Dotenv . load ();
OkHttpClient client = new OkHttpClient ();
String key = dotenv . get ( "ORDERLY_SECRET" );
EdDSAParameterSpec spec = EdDSANamedCurveTable . getByName ( EdDSANamedCurveTable . ED_25519 );
EdDSAPrivateKeySpec encoded = new EdDSAPrivateKeySpec ( Base58 . decode (key), spec);
EdDSAPrivateKey orderlyKey = new EdDSAPrivateKey (encoded);
Signer signer = new Signer (baseUrl, orderlyAccountId, orderlyKey);
JSONObject json = new JSONObject ();
json . put ( "symbol" , "PERP_ETH_USDC" );
json . put ( "order_type" , "MARKET" );
json . put ( "order_quantity" , 0.01 );
json . put ( "side" , "BUY" );
Request req = signer . createSignedRequest ( "/v1/order" , "POST" , json);
String res ;
try ( Response response = client . newCall (req). execute ()) {
res = response . body (). string ();
}
JSONObject obj = new JSONObject (res);
}
}
authentication_example.py
signer.py
import json
import os
from base58 import b58decode
from requests import Request, Session
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from signer import Signer
base_url = "https://testnet-api.perpo.trade"
orderly_account_id = "<perpo-account-id>"
key = b58decode(os.environ.get( "ORDERLY_SECRET" ))
orderly_key = Ed25519PrivateKey.from_private_bytes(key)
session = Session()
signer = Signer(orderly_account_id, orderly_key)
req = signer.sign_request(
Request(
"POST" ,
" %s /v1/order" % base_url,
json = {
"symbol" : "PERP_ETH_USDC" ,
"order_type" : "MARKET" ,
"order_quantity" : 0.01 ,
"side" : "BUY" ,
},
)
)
res = session.send(req)
response = json.loads(res.text)
authenticationExample.ts
signer.ts
import bs58 from 'bs58' ;
import { config } from 'dotenv' ;
import { webcrypto } from 'node:crypto' ;
import { signAndSendRequest } from "./signer" ;
// this is only necessary in Node.js to make `@noble/ed25519` dependency work
if ( ! globalThis . crypto ) globalThis . crypto = webcrypto as any ;
config ();
async function main () {
const baseUrl = 'https://testnet-api.perpo.trade' ;
const orderlyAccountId = '<perpo-account-id>' ;
const orderlyKey = bs58 . decode ( process . env . ORDERLY_SECRET ! );
const res = await signAndSendRequest ( orderlyAccountId , orderlyKey , ` ${ baseUrl } /v1/order` , {
method: 'POST' ,
body: JSON . stringify ({
symbol: 'PERP_ETH_USDC' ,
order_type: 'MARKET' ,
order_quantity: 0.01 ,
side: 'BUY'
})
});
const response = await res . json ();
console . log ( response );
}
main ();
curl --request POST \
--url https://api.perpo.trade/v1/order \
--header 'Content-Type: application/json' \
--header 'perpo-account-id: <perpo-account-id>' \
--header 'perpo-key: ed25519:8tm7dnKYkSc3FzgPuJaw1wztr79eeZpN35nHW5pL5XhX' \
--header 'perpo-signature: dG4bkKiqG0dUYLzViRZkvbI6Sy239JxAdNMIBxFZ4w030Jofr0ORV06GHtvXZkaZaWUXE+XAU3fnzKN/5fDeBQ==' \
--header 'perpo-timestamp: 1649920583000' \
--data '{
"symbol": "<symbol>",
"order_type": "<order_type>",
"side": "<side>",
"reduce_only": false
}'
Security
Perpo validates every request through three checks. A request must pass all three to be accepted.
Request Timestamp
The request is rejected if the perpo-timestamp header differs from the API server time by more than 300 seconds.
Signature Verification
The perpo-signature header must be a valid ed25519 signature generated from the normalized request content and signed with your Perpo secret key.
Perpo Key Validity
The perpo-key header must reference a key that has been added to the network, is associated with the account, and has not expired.