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. For account-scoped endpoints (trading, positions, reports), you must also include the x-participant-id header.

REST API

curl -X GET "https://api.preprod.polymarketexchange.com/v1/whoami" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "x-participant-id: firms/YourFirm/users/your-user"

gRPC

import grpc

# Create metadata with token
metadata = [
    ('authorization', f'Bearer {access_token}'),
    ('x-participant-id', 'firms/YourFirm/users/your-user')
]

# Make gRPC call with metadata
response = stub.SomeMethod(request, metadata=metadata)
Verify your token scopes and ensure x-participant-id is included for account-scoped endpoints. If you don’t know your participant ID, call GET /v1/whoami or GET /v1/users and put your firm and user into the firms/<YOURFIRM>/users/<USER> format. Note you will have one firm but can have multiple users.

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

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:marketdataBBO (best bid/offer) and market data subscriptions (including BiDirectionalStreamMarketData)
read:l2marketdataL2 orderbook depth (premium)
read:instrumentsRefData, instrument listings and metadata
read:ordersView open orders, preview orders, order subscriptions, combo RFQs, quotes, and RFQ event streams
write:ordersInsert / cancel / replace / modify orders; create, quote, delete, and accept combo RFQs
read:reportsSearch/download orders, trades, executions, and incentives earnings
read:positionsPosition queries, balance queries, position ledger, balance ledger, and position valuations (including historical via as_of_time / as_of_date)
read:dropcopyDrop copy subscriptions
read:accountsView users (/v1/whoami, /v1/users) and account info
read:fundingView funding sources and transactions
write:fundingUpdate funding, create deposits and withdrawals
Strict scope enforcement. Calls that are missing a required scope fail with 403 Forbidden (REST) / PERMISSION_DENIED (gRPC) and the message permission denied: missing required scope <scope>. Balance ledger endpoints use read:positions (not read:funding) to stay consistent with the existing balance-query endpoints (GetAccountBalance, ListAccountBalances).

Scope Requirements by Endpoint

EndpointMethodRequired Scope
/v1/trading/ordersPOSTwrite:orders
/v1/trading/orders/cancelPOSTwrite:orders
/v1/trading/orders/openGETread:orders
/v1/combos/rfq/user-idGETread:orders
/v1/combos/rfqsGETread:orders
/v1/combos/rfqsPOSTwrite:orders
/v1/combos/quotesGETread:orders
/v1/combos/quotesPOSTwrite:orders
/v1/combos/rfqs/{rfqId}/quotes/{quoteId}DELETEwrite:orders
/v1/combos/rfqs/{rfqId}/quotes/{quoteId}/acceptPUTwrite:orders
StreamRFQEvents (gRPC)read:orders
/v1/report/orders/searchPOSTread:reports
/v1/report/trades/searchPOSTread:reports
/v1/incentives/earningsGETread:reports  (disabled in preprod)
/v1/positionsGETread:positions
/v1/positions/balancePOSTread:positions
/v1/positions/balancesPOSTread:positions
/v1/positions/ledgerGETread:positions
/v1/positions/ledger/downloadGETread:positions
/v1/funding/balance-ledgerGETread:positions
/v1/funding/balance-ledger/downloadGETread:positions
CreateBalanceLedgerSubscription (gRPC)read:positions
/v1/valuations/positionsGETread:positions
/v1/valuations/positions/downloadGETread:positions
/v1/valuations/accounts/statement/downloadPOSTread:positions
/v1/orderbook/{symbol}GETread:l2marketdata
/v1/orderbook/{symbol}/bboGETread:marketdata
BiDirectionalStreamMarketData (gRPC)read:marketdata
CreateMarketDataSubscription (gRPC)read:marketdata
/v1/refdata/symbolsPOSTread:instruments
/v1/refdata/instrumentsPOSTread:instruments
/v1/refdata/metadataPOSTread:instruments
/v1/whoamiGETread:accounts
/v1/usersGETread:accounts
/v1/funding/accountsGETread:funding
/v1/aeropay/depositsPOSTwrite:funding
/v1/checkout/depositsPOSTwrite:funding
/v1/healthGET(no auth required)
gRPC streams (BiDirectionalStreamMarketData, CreateMarketDataSubscription, CreateBalanceLedgerSubscription, StreamRFQEvents) run on a separate ALB (grpc-api.{env}.polymarketexchange.com:443) that bypasses the API Gateway and its 30-second idle timeout. Scope validation for these streams happens at the application layer rather than at the load balancer, but the resulting PERMISSION_DENIED behavior is identical to REST endpoints.
/v1/incentives/earnings is currently disabled in preprod and returns a route-not-found error there. The endpoint is live in dev01 and prod. Earnings flow shape can still be validated against the OpenAPI schema and the incentives overview.

Permission Denied Response

When a request’s token is missing the required scope:
{
  "code": 7,
  "message": "permission denied: missing required scope read:positions"
}
SurfaceStatus / Code
REST403 Forbidden
gRPCPERMISSION_DENIED (code 7)
If you receive this error, update your Auth0 application to include the missing scope and request a fresh access token.

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: