Skip to main content
The Polymarket Exchange API uses Private Key JWT authentication with RSA keys. You sign a JWT with your RSA private key and exchange it for an access token.
Complete Onboarding first to generate your keys and receive your Client ID.

Environments

EnvironmentAuth DomainAPI Domain
Developmentpmx-dev01.us.auth0.comapi.dev01.polymarketexchange.com
Pre-productionpmx-preprod.us.auth0.comapi.preprod.polymarketexchange.com
Productionpmx-prod.us.auth0.comapi.prod.polymarketexchange.com
Use https://[API Domain] for both the JWT audience claim and API base URL.
Each environment requires separate onboarding. Your pre-production credentials will not work in production.

How It Works

Authentication follows these steps:
  1. Create a signed JWT assertion - Sign a JWT with your private key
  2. Exchange for API access token - Send the assertion to the token endpoint
  3. Call API with access token - Include the token in your API requests

Prerequisites

After completing Onboarding, you will have:
You HaveFrom Onboarding
Private key fileGenerated by you (keep secure!)
Client IDProvided by Polymarket via clientid.txt in your shared Google Drive folder
Auth DomainSee Environments
API AudienceSee Environments

Create Client Assertion JWT

Create a JWT with these claims, signed with your private key using RS256:
{
  "iss": "YOUR_CLIENT_ID",
  "sub": "YOUR_CLIENT_ID",
  "aud": "https://pmx-preprod.us.auth0.com/oauth/token",
  "iat": 1703270400,
  "exp": 1703270700,
  "jti": "unique-random-uuid"
}
ClaimDescription
issYour client ID (issuer)
subYour client ID (subject)
audToken endpoint URL
iatIssued at time (Unix timestamp)
expExpiration time (max 5 minutes from iat)
jtiUnique token ID (prevents replay attacks)

Request Access Token

curl --request POST \
  --url "https://pmx-preprod.us.auth0.com/oauth/token" \
  --header "content-type: application/json" \
  --data '{
    "client_id": "YOUR_CLIENT_ID",
    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "client_assertion": "YOUR_SIGNED_JWT_ASSERTION",
    "audience": "https://api.preprod.polymarketexchange.com",
    "grant_type": "client_credentials"
  }'

Token Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs...",
  "token_type": "Bearer",
  "expires_in": 180
}

Complete Python Example

import jwt
import uuid
import time
import requests
from cryptography.hazmat.primitives import serialization

class AuthClient:
    def __init__(self, domain: str, client_id: str, audience: str, private_key_path: str):
        self.domain = domain
        self.client_id = client_id
        self.audience = audience
        self.private_key_path = private_key_path
        self.token = None
        self.token_expiry = None

    def _load_private_key(self):
        """Load the RSA private key from file."""
        with open(self.private_key_path, 'rb') as f:
            return serialization.load_pem_private_key(f.read(), password=None)

    def _create_client_assertion(self) -> str:
        """Create a signed JWT for client authentication."""
        private_key = self._load_private_key()
        now = int(time.time())

        claims = {
            "iss": self.client_id,
            "sub": self.client_id,
            "aud": f"https://{self.domain}/oauth/token",
            "iat": now,
            "exp": now + 300,  # 5 minutes
            "jti": str(uuid.uuid4()),
        }

        return jwt.encode(claims, private_key, algorithm="RS256")

    def get_token(self) -> str:
        """Get a valid access token, refreshing if necessary."""
        if self._is_token_valid():
            return self.token

        # Create client assertion
        assertion = self._create_client_assertion()

        # Request access token
        response = requests.post(
            f"https://{self.domain}/oauth/token",
            json={
                "client_id": self.client_id,
                "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                "client_assertion": assertion,
                "audience": self.audience,
                "grant_type": "client_credentials"
            },
            headers={"content-type": "application/json"}
        )
        response.raise_for_status()

        data = response.json()
        self.token = data["access_token"]
        # Set expiry with 30-second buffer
        self.token_expiry = time.time() + data["expires_in"] - 30

        return self.token

    def _is_token_valid(self) -> bool:
        """Check if current token is still valid."""
        if not self.token or not self.token_expiry:
            return False
        return time.time() < self.token_expiry


# Usage
auth_client = AuthClient(
    domain="pmx-preprod.us.auth0.com",
    client_id="YOUR_CLIENT_ID",
    audience="https://api.preprod.polymarketexchange.com",
    private_key_path="/path/to/my_private_key.pem"
)

token = auth_client.get_token()
Required packages:
pip install PyJWT cryptography requests

Complete Go Example

package main

import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/json"
    "encoding/pem"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/google/uuid"
)

func getAccessToken(domain, clientID, audience, privateKeyPath string) (string, error) {
    // Load private key
    keyData, err := os.ReadFile(privateKeyPath)
    if err != nil {
        return "", fmt.Errorf("read key file: %w", err)
    }

    block, _ := pem.Decode(keyData)
    privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return "", fmt.Errorf("parse private key: %w", err)
    }

    // Create client assertion JWT
    now := time.Now()
    claims := jwt.MapClaims{
        "iss": clientID,
        "sub": clientID,
        "aud": fmt.Sprintf("https://%s/oauth/token", domain),
        "iat": now.Unix(),
        "exp": now.Add(5 * time.Minute).Unix(),
        "jti": uuid.New().String(),
    }

    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    assertion, err := token.SignedString(privateKey)
    if err != nil {
        return "", fmt.Errorf("sign assertion: %w", err)
    }

    // Request access token
    // (implement HTTP POST to token endpoint)
    // ...

    return accessToken, nil
}

Using the Access Token

Include the access token in the Authorization header for all API requests:

REST API

curl -X GET "https://api.preprod.polymarketexchange.com/v1/whoami" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

gRPC

import grpc

# Create metadata with token
metadata = [
    ('authorization', f'Bearer {access_token}')
]

# Make gRPC call with metadata
response = stub.SomeMethod(request, metadata=metadata)

Key Rotation

You can rotate your keys at any time:
  1. Generate a new key pair
  2. Complete a new Onboarding submission with the new public key
  3. We add the new key to your application
  4. Update your systems to use the new private key
  5. Notify us to remove the old public key

Troubleshooting

Common Errors

ErrorCauseSolution
invalid_clientJWT signature verification failedVerify private key matches registered public key
invalid_client_assertionMalformed JWT or wrong claimsCheck JWT claims (iss, sub, aud, exp)
401 UnauthorizedInvalid or expired access tokenRequest a new access token
403 ForbiddenIP not allowlistedContact support to add your IP

Debugging JWT Claims

If authentication fails, verify your client assertion JWT contains correct claims:
{
  "iss": "YOUR_CLIENT_ID",
  "sub": "YOUR_CLIENT_ID",
  "aud": "https://pmx-preprod.us.auth0.com/oauth/token",
  "iat": 1703270400,
  "exp": 1703270700,
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}
Common mistakes:
  • Wrong aud (must be the token endpoint, not the API)
  • Expired JWT (exp in the past)
  • Reused jti (must be unique per request)

API Scopes

Your application is granted specific scopes that control which API endpoints you can access. Scopes are included in your access token and validated by the API.

Available Scopes

ScopeDescription
read:marketdataAccess BBO (best bid/offer) and streaming market data
read:l2marketdataAccess L2 orderbook depth (premium)
read:instrumentsList instruments and metadata
read:ordersView open orders, preview orders
write:ordersCreate and cancel orders
read:reportsSearch orders/trades/executions, download reports
read:positionsView account positions and balances
read:dropcopyDrop copy subscriptions
read:accountsView account info
read:fundingView funding accounts and transactions
write:fundingCreate deposits and withdrawals

Scope Requirements by Endpoint

EndpointMethodRequired Scope
/v1/trading/ordersPOSTwrite:orders
/v1/trading/orders/cancelPOSTwrite:orders
/v1/trading/open-ordersGETread:orders
/v1/report/orders/searchPOSTread:reports
/v1/report/trades/searchPOSTread:reports
/v1/positionsGETread:positions
/v1/positions/balancePOSTread:positions
/v1/positions/balancesPOSTread:positions
/v1/orderbook/{symbol}GETread:l2marketdata
/v1/orderbook/{symbol}/bboGETread:marketdata
/v1/refdata/symbolsPOSTread:instruments
/v1/refdata/instrumentsPOSTread:instruments
/v1/refdata/metadataPOSTread:instruments
/v1/accounts/whoamiGETread:accounts
/v1/funding/accountsGETread:funding
/v1/aeropay/depositsPOSTwrite:funding
/v1/checkout/depositsPOSTwrite:funding
/v1/healthGET(no auth required)

Checking Your Scopes

Your granted scopes are included in your access token. You can decode the token to see them:
import base64
import json

# Decode the payload (middle part of JWT)
payload = access_token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)  # Add padding
claims = json.loads(base64.urlsafe_b64decode(payload))

print("Granted scopes:", claims.get("scope", ""))
If you receive a 403 Forbidden error, check that your application has been granted the required scope for that endpoint. Contact support to request additional scopes.

Additional Resources

For more details on Private Key JWT authentication: