Skip to content

Authentication

PAX uses Ed25519 public key signatures to authenticate API requests. Your private key never leaves your machine. PAX only stores your public key and verifies signatures server-side.

Log in to PAX (or sandbox), navigate to Settings, and click Create Key and choose permissions.

Generate an Ed25519 keypair locally and paste your public key (PEM-encoded) into the form. Your private key is never sent to PAX. After creation, PAX returns a key ID (e.g. pax_live_a3b4c5d6e7f8) that identifies your key in API requests.

This script saves the private key to my-private-key.pem, readable only by you, and prints the public key to paste:

import os
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
private_key = Ed25519PrivateKey.generate()
with open(os.open("my-private-key.pem", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as f:
f.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()))
print(private_key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode())

Every authenticated request requires three headers:

Header Value
api-access-key Your key ID
api-timestamp Current Unix timestamp in milliseconds
api-signature Base64-encoded Ed25519 signature

The signature message is the concatenation of four components with no separator:

timestamp + method + path + body

Where timestamp matches the header value, method is uppercase (e.g. POST), path is the request path with its query string, percent-encoded exactly as sent (e.g. /v1/order/submit?recvWindow=20000), and body is the request body exactly as sent, byte for byte. Use an empty string as the body for GET and DELETE requests.

import time, json, base64, requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
# Load your private key and key ID (from key creation step)
private_key = load_pem_private_key(open("my-private-key.pem", "rb").read(), password=None)
key_id = "pax_test_a3b4c5d6e7f8"
timestamp = str(int(time.time() * 1000))
method = "POST"
path = "/v1/order/submit"
body = json.dumps({"order": {"side": 2, "market_id": "1", "qty": "10000", "ticks": 50000, "tif": 2}})
message = f"{timestamp}{method}{path}{body}"
signature = base64.b64encode(private_key.sign(message.encode())).decode("ascii")
headers = {
"api-access-key": key_id,
"api-timestamp": timestamp,
"api-signature": signature,
"Content-Type": "application/json",
}
response = requests.post(f"https://api-sandbox.pax.markets{path}", data=body, headers=headers)

PAX checks api-timestamp so a signed request is only valid briefly. Timestamps must not be more than 1 second in the future or older than the recvWindow (default 5000 ms, maximum 60000 ms). Override the default by adding recvWindow as a query parameter:

POST /v1/order/submit?recvWindow=20000

Regulated services provided by 1Money USA, Inc., a licensed money transmitter, NMLS ID 2628653 · Licenses