> ## Documentation Index
> Fetch the complete documentation index at: https://partners.docs.polymarket.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Canonical token client

> The token client the rest of this space imports, in Python, TypeScript, bash and Go, plus the gRPC channel and per-call metadata helpers.

The snippets below are the client the rest of this space imports. Save the file under the name in the first comment line; later pages show the import line rather than repeating the auth code.

<CodeGroup>
  ```python polymarket_auth.py theme={null}
  # pip install "pyjwt[crypto]" requests grpcio
  #
  # Canonical Polymarket US token client. Other pages in this space do:
  #   from polymarket_auth import PolymarketClient

  import time
  import uuid

  import grpc
  import jwt  # PyJWT
  import requests

  # --- Values that vary per partner -------------------------------------------
  # From us at your credential gate (see "Generate your keys").
  CLIENT_ID = "REPLACE_WITH_YOUR_CLIENT_ID"
  # The private half of the keypair you generated (see "Generate your keys"). Never leaves your infra.
  PRIVATE_KEY_PATH = "yourfirm-preprod-private.pem"
  # Preprod values from "Environments and endpoints". Production:
  #   https://pmx-prod.us.auth0.com/oauth/token and https://api.prod.polymarketexchange.com
  TOKEN_ENDPOINT = "https://pmx-preprod.us.auth0.com/oauth/token"
  API_BASE_URL = "https://api.preprod.polymarketexchange.com"
  # Preprod gRPC target from "Environments and endpoints". Production:
  #   grpc-api.prod.polymarketexchange.com:443
  GRPC_TARGET = "grpc-api.preprod.polymarketexchange.com:443"
  # ---------------------------------------------------------------------------

  ASSERTION_TTL_SECONDS = 300  # exp may be at most 5 minutes after iat
  REFRESH_BUFFER_SECONDS = 30  # re-mint this long before expires_in elapses

  class PolymarketClient:
      def __init__(self, client_id=CLIENT_ID, private_key_path=PRIVATE_KEY_PATH,
                   token_endpoint=TOKEN_ENDPOINT, api_base_url=API_BASE_URL,
                   grpc_target=GRPC_TARGET):
          self.client_id = client_id
          self.token_endpoint = token_endpoint
          self.api_base_url = api_base_url
          self.grpc_target = grpc_target
          with open(private_key_path, "rb") as fh:
              self.private_key = fh.read()
          self._token = None
          self._expires_at = 0.0

      def _client_assertion(self):
          now = int(time.time())
          return jwt.encode(
              {
                  "iss": self.client_id,
                  "sub": self.client_id,
                  # aud is the TOKEN ENDPOINT, not the API base URL.
                  "aud": self.token_endpoint,
                  "iat": now,
                  "exp": now + ASSERTION_TTL_SECONDS,
                  "jti": str(uuid.uuid4()),  # unique per assertion
              },
              self.private_key,
              algorithm="RS256",
          )

      def access_token(self):
          if self._token and time.time() < self._expires_at:
              return self._token

          resp = requests.post(
              self.token_endpoint,
              data={
                  "grant_type": "client_credentials",
                  "client_id": self.client_id,
                  "client_assertion_type":
                      "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                  "client_assertion": self._client_assertion(),
                  # audience is the API BASE URL. Do not send `scope`.
                  "audience": self.api_base_url,
              },
              timeout=10,
          )
          resp.raise_for_status()  # never swallow a token failure
          body = resp.json()

          self._token = body["access_token"]
          # Honour expires_in minus a 30s buffer. Never hardcode 180.
          self._expires_at = time.time() + int(body["expires_in"]) - REFRESH_BUFFER_SECONDS
          return self._token

      def auth_metadata(self):
          """Header pair for REST headers. gRPC calls use metadata() below."""
          return (("authorization", f"Bearer {self.access_token()}"),)

      # --- gRPC -----------------------------------------------------------------

      def channel(self):
          """Secure channel to the gRPC target. TLS only — auth travels in metadata.

          Use it as a context manager:  with client.channel() as ch: ...
          """
          return grpc.secure_channel(self.grpc_target, grpc.ssl_channel_credentials())

      def metadata(self, participant_id=None):
          """Per-call gRPC metadata.

          access_token() re-mints at expires_in minus 30 s, so every call gets a
          live token. Pass participant_id ONLY on account-scoped calls.
          """
          md = [("authorization", f"Bearer {self.access_token()}")]
          if participant_id:
              md.append(("x-participant-id", participant_id))
          return tuple(md)

      def get(self, path):
          resp = requests.get(
              f"{self.api_base_url}{path}",
              headers={"authorization": f"Bearer {self.access_token()}"},
              timeout=10,
          )
          resp.raise_for_status()
          return resp.json()

  if __name__ == "__main__":
      client = PolymarketClient()
      print(client.get("/v1/whoami"))
  ```

  ```typescript polymarketAuth.ts theme={null}
  // npm i jose @grpc/grpc-js        (Node 18+ — global fetch)
  //
  // Canonical Polymarket US token client. Other pages in this space do:
  //   import { PolymarketClient } from "./polymarketAuth";

  import { readFileSync } from "node:fs";
  import { randomUUID } from "node:crypto";
  import { SignJWT, importPKCS8 } from "jose";
  import { Channel, ChannelCredentials, Metadata } from "@grpc/grpc-js";

  // --- Values that vary per partner -------------------------------------------
  // From us at your credential gate (see "Generate your keys").
  const CLIENT_ID = "REPLACE_WITH_YOUR_CLIENT_ID";
  // The private half of the keypair you generated (see "Generate your keys").
  // importPKCS8 needs a "-----BEGIN PRIVATE KEY-----" file. If yours says
  // "BEGIN RSA PRIVATE KEY", convert it once:
  //   openssl pkcs8 -topk8 -nocrypt -in in.pem -out out.pem
  const PRIVATE_KEY_PATH = "yourfirm-preprod-private.pem";
  // Preprod values from "Environments and endpoints". Production:
  //   https://pmx-prod.us.auth0.com/oauth/token and https://api.prod.polymarketexchange.com
  const TOKEN_ENDPOINT = "https://pmx-preprod.us.auth0.com/oauth/token";
  const API_BASE_URL = "https://api.preprod.polymarketexchange.com";
  // Preprod gRPC target from "Environments and endpoints". Production:
  //   grpc-api.prod.polymarketexchange.com:443
  const GRPC_TARGET = "grpc-api.preprod.polymarketexchange.com:443";
  // ---------------------------------------------------------------------------

  const ASSERTION_TTL_SECONDS = 300; // exp may be at most 5 minutes after iat
  const REFRESH_BUFFER_SECONDS = 30; // re-mint this long before expires_in elapses

  export class PolymarketClient {
    private token: string | null = null;
    private expiresAtMs = 0;

    constructor(
      private clientId = CLIENT_ID,
      private privateKeyPath = PRIVATE_KEY_PATH,
      private tokenEndpoint = TOKEN_ENDPOINT,
      private apiBaseUrl = API_BASE_URL,
      private grpcTarget = GRPC_TARGET,
    ) {}

    private async clientAssertion(): Promise<string> {
      const pem = readFileSync(this.privateKeyPath, "utf8");
      const key = await importPKCS8(pem, "RS256");
      const now = Math.floor(Date.now() / 1000);

      return new SignJWT({ jti: randomUUID() }) // unique per assertion
        .setProtectedHeader({ alg: "RS256", typ: "JWT" })
        .setIssuer(this.clientId)
        .setSubject(this.clientId)
        // aud is the TOKEN ENDPOINT, not the API base URL.
        .setAudience(this.tokenEndpoint)
        .setIssuedAt(now)
        .setExpirationTime(now + ASSERTION_TTL_SECONDS)
        .sign(key);
    }

    async accessToken(): Promise<string> {
      if (this.token && Date.now() < this.expiresAtMs) return this.token;

      const body = new URLSearchParams({
        grant_type: "client_credentials",
        client_id: this.clientId,
        client_assertion_type:
          "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        client_assertion: await this.clientAssertion(),
        // audience is the API BASE URL. Do not send `scope`.
        audience: this.apiBaseUrl,
      });

      const res = await fetch(this.tokenEndpoint, {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body,
      });
      if (!res.ok) {
        throw new Error(`token endpoint ${res.status}: ${await res.text()}`);
      }

      const json = (await res.json()) as { access_token: string; expires_in: number };
      this.token = json.access_token;
      // Honour expires_in minus a 30s buffer. Never hardcode 180.
      this.expiresAtMs = Date.now() + (json.expires_in - REFRESH_BUFFER_SECONDS) * 1000;
      return this.token;
    }

    /** Header pair for REST headers. gRPC calls use metadata() below. */
    async authMetadata(): Promise<Record<string, string>> {
      return { authorization: `Bearer ${await this.accessToken()}` };
    }

    // --- gRPC ------------------------------------------------------------------

    /** Secure channel to the gRPC target. TLS only — auth travels in metadata. */
    channel(): Channel {
      return new Channel(this.grpcTarget, ChannelCredentials.createSsl(), {});
    }

    /**
     * Per-call gRPC metadata. accessToken() re-mints at expires_in minus 30 s,
     * so every call gets a live token. Pass participantId ONLY on
     * account-scoped calls.
     */
    async metadata(participantId?: string): Promise<Metadata> {
      const md = new Metadata();
      md.set("authorization", `Bearer ${await this.accessToken()}`);
      if (participantId) md.set("x-participant-id", participantId);
      return md;
    }

    async get<T>(path: string): Promise<T> {
      const res = await fetch(`${this.apiBaseUrl}${path}`, {
        headers: await this.authMetadata(),
      });
      if (!res.ok) throw new Error(`GET ${path} ${res.status}: ${await res.text()}`);
      return (await res.json()) as T;
    }
  }

  if (process.argv[1]?.endsWith("polymarketAuth.ts")) {
    new PolymarketClient().get("/v1/whoami").then(console.log, (e) => {
      console.error(e);
      process.exit(1);
    });
  }
  ```

  ```bash token.sh theme={null}
  #!/usr/bin/env bash
  # Requires: openssl, curl, jq. No other dependencies.
  set -euo pipefail

  # --- Values that vary per partner -------------------------------------------
  # From us at your credential gate (see "Generate your keys").
  CLIENT_ID="REPLACE_WITH_YOUR_CLIENT_ID"
  # The private half of the keypair you generated (see "Generate your keys").
  PRIVATE_KEY="yourfirm-preprod-private.pem"
  # Preprod values from "Environments and endpoints". Production:
  #   https://pmx-prod.us.auth0.com/oauth/token and https://api.prod.polymarketexchange.com
  TOKEN_ENDPOINT="https://pmx-preprod.us.auth0.com/oauth/token"
  API_BASE_URL="https://api.preprod.polymarketexchange.com"
  # ---------------------------------------------------------------------------

  ASSERTION_TTL_SECONDS=300   # exp may be at most 5 minutes after iat

  b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }

  NOW=$(date +%s)
  JTI=$(openssl rand -hex 16)   # unique per assertion

  HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | b64url)
  # aud is the TOKEN ENDPOINT, not the API base URL.
  PAYLOAD=$(printf '{"iss":"%s","sub":"%s","aud":"%s","iat":%s,"exp":%s,"jti":"%s"}' \
    "$CLIENT_ID" "$CLIENT_ID" "$TOKEN_ENDPOINT" "$NOW" "$((NOW + ASSERTION_TTL_SECONDS))" "$JTI" \
    | b64url)
  SIGNING_INPUT="${HEADER}.${PAYLOAD}"
  SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign "$PRIVATE_KEY" | b64url)
  ASSERTION="${SIGNING_INPUT}.${SIGNATURE}"

  # --fail-with-body: non-2xx exits non-zero and still prints the error body.
  TOKEN_RESPONSE=$(curl -sS --fail-with-body -X POST "$TOKEN_ENDPOINT" \
    -H 'content-type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode "client_id=${CLIENT_ID}" \
    --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
    --data-urlencode "client_assertion=${ASSERTION}" \
    --data-urlencode "audience=${API_BASE_URL}")   # audience is the API BASE URL; no `scope`

  ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -re .access_token)
  EXPIRES_IN=$(printf '%s' "$TOKEN_RESPONSE" | jq -re .expires_in)
  # Honour expires_in minus a 30s buffer. Never hardcode 180.
  echo "re-mint after $((EXPIRES_IN - 30))s" >&2

  # stdout is exactly the token, so every other snippet in this space can do:
  #   TOKEN="$(./token.sh)"
  printf '%s\n' "$ACCESS_TOKEN"

  # Demo REST call. On stderr, so it never contaminates the token on stdout.
  curl -sS --fail-with-body "${API_BASE_URL}/v1/whoami" \
    -H "authorization: Bearer ${ACCESS_TOKEN}" >&2
  ```

  ```go pmauth/client.go theme={null}
  // go get github.com/golang-jwt/jwt/v5 github.com/google/uuid google.golang.org/grpc
  //
  // Canonical Polymarket US token client. Other pages in this space do:
  //   import "example.com/yourfirm/pmauth"

  package pmauth

  import (
  	"crypto/tls"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"net/url"
  	"os"
  	"strings"
  	"sync"
  	"time"

  	"github.com/golang-jwt/jwt/v5"
  	"github.com/google/uuid"
  	"google.golang.org/grpc"
  	"google.golang.org/grpc/credentials"
  	"google.golang.org/grpc/metadata"
  )

  // --- Values that vary per partner -------------------------------------------
  const (
  	// From us at your credential gate (see "Generate your keys").
  	ClientID = "REPLACE_WITH_YOUR_CLIENT_ID"
  	// The private half of the keypair you generated (see "Generate your keys").
  	PrivateKeyPath = "yourfirm-preprod-private.pem"
  	// Preprod values from "Environments and endpoints". Production:
  	//   https://pmx-prod.us.auth0.com/oauth/token and https://api.prod.polymarketexchange.com
  	TokenEndpoint = "https://pmx-preprod.us.auth0.com/oauth/token"
  	APIBaseURL    = "https://api.preprod.polymarketexchange.com"
  	// Preprod gRPC target from "Environments and endpoints". Production:
  	//   grpc-api.prod.polymarketexchange.com:443
  	GRPCTarget = "grpc-api.preprod.polymarketexchange.com:443"
  )

  // ---------------------------------------------------------------------------

  const (
  	assertionTTL   = 5 * time.Minute  // exp may be at most 5 minutes after iat
  	refreshBuffer  = 30 * time.Second // re-mint this long before expires_in elapses
  )

  type Client struct {
  	mu        sync.Mutex
  	token     string
  	expiresAt time.Time
  	HTTP      *http.Client
  }

  func New() *Client {
  	return &Client{HTTP: &http.Client{Timeout: 10 * time.Second}}
  }

  func (c *Client) clientAssertion() (string, error) {
  	pem, err := os.ReadFile(PrivateKeyPath)
  	if err != nil {
  		return "", fmt.Errorf("read private key: %w", err)
  	}
  	key, err := jwt.ParseRSAPrivateKeyFromPEM(pem)
  	if err != nil {
  		return "", fmt.Errorf("parse private key: %w", err)
  	}
  	now := time.Now()
  	return jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
  		"iss": ClientID,
  		"sub": ClientID,
  		// aud is the TOKEN ENDPOINT, not the API base URL.
  		"aud": TokenEndpoint,
  		"iat": now.Unix(),
  		"exp": now.Add(assertionTTL).Unix(),
  		"jti": uuid.NewString(), // unique per assertion
  	}).SignedString(key)
  }

  func (c *Client) AccessToken() (string, error) {
  	c.mu.Lock()
  	defer c.mu.Unlock()
  	if c.token != "" && time.Now().Before(c.expiresAt) {
  		return c.token, nil
  	}

  	assertion, err := c.clientAssertion()
  	if err != nil {
  		return "", err
  	}

  	form := url.Values{
  		"grant_type":            {"client_credentials"},
  		"client_id":             {ClientID},
  		"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
  		"client_assertion":      {assertion},
  		// audience is the API BASE URL. Do not send "scope".
  		"audience": {APIBaseURL},
  	}
  	resp, err := c.HTTP.Post(TokenEndpoint,
  		"application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
  	if err != nil {
  		return "", err
  	}
  	defer resp.Body.Close()
  	body, _ := io.ReadAll(resp.Body)
  	if resp.StatusCode != http.StatusOK {
  		return "", fmt.Errorf("token endpoint %d: %s", resp.StatusCode, body)
  	}

  	var out struct {
  		AccessToken string `json:"access_token"`
  		ExpiresIn   int    `json:"expires_in"`
  	}
  	if err := json.Unmarshal(body, &out); err != nil {
  		return "", err
  	}
  	c.token = out.AccessToken
  	// Honour expires_in minus a 30s buffer. Never hardcode 180.
  	c.expiresAt = time.Now().Add(time.Duration(out.ExpiresIn)*time.Second - refreshBuffer)
  	return c.token, nil
  }

  // AuthMetadata is the header pair for REST headers. gRPC calls use Metadata.
  func (c *Client) AuthMetadata() (string, string, error) {
  	tok, err := c.AccessToken()
  	return "authorization", "Bearer " + tok, err
  }

  // --- gRPC -------------------------------------------------------------------

  // Channel dials the gRPC target over TLS. Auth travels in metadata, not on the
  // channel. Close it when you are done with it.
  func (c *Client) Channel() (*grpc.ClientConn, error) {
  	return grpc.NewClient(GRPCTarget, grpc.WithTransportCredentials(
  		credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})))
  }

  // Metadata returns the per-call gRPC metadata. AccessToken re-mints at
  // expires_in minus 30 s, so every call gets a live token. Pass participantID
  // ONLY on account-scoped calls; pass "" everywhere else.
  func (c *Client) Metadata(participantID string) (metadata.MD, error) {
  	tok, err := c.AccessToken()
  	if err != nil {
  		return nil, err
  	}
  	md := metadata.Pairs("authorization", "Bearer "+tok)
  	if participantID != "" {
  		md.Set("x-participant-id", participantID)
  	}
  	return md, nil
  }

  func (c *Client) Get(path string) ([]byte, error) {
  	tok, err := c.AccessToken()
  	if err != nil {
  		return nil, err
  	}
  	req, _ := http.NewRequest(http.MethodGet, APIBaseURL+path, nil)
  	req.Header.Set("authorization", "Bearer "+tok)
  	resp, err := c.HTTP.Do(req)
  	if err != nil {
  		return nil, err
  	}
  	defer resp.Body.Close()
  	body, _ := io.ReadAll(resp.Body)
  	if resp.StatusCode != http.StatusOK {
  		return nil, fmt.Errorf("GET %s %d: %s", path, resp.StatusCode, body)
  	}
  	return body, nil
  }
  ```
</CodeGroup>

### Token response

```json theme={null}
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
  "expires_in": 180,
  "token_type": "Bearer"
}
```

Branch on `expires_in`, not on a constant.

### `GET /v1/whoami` response

```json theme={null}
{
  "ep3_account_firm_name": "firms/20260821-examplefirminc-api-clearing-member"
}
```

<Warning>
  **`whoami` returns your clearing-member firm, not your participant firm.** Your end users live under
  the participant firm. Do not build a participant ID from this value — it raises a security alert
  against your own account. Read [Firms, participants and accounts](/identity) before you use
  this response for anything.

  The complete field list for this response is not yet published; branch only on
  `ep3_account_firm_name`.
</Warning>

<Info>
  **API reference:** <a href="https://docs.polymarket.us/trader-guide/authentication" target="_blank" rel="noreferrer">private\_key\_jwt flow</a>. Opens on the public documentation site in a new tab.
  Where it disagrees with this page, this page is authoritative for the partner surface.
</Info>

## The gRPC channel and the per-call metadata

Most of this space is gRPC, so the same module publishes one channel helper and one metadata helper. Every gRPC snippet on every later page imports these two and nothing else:

| Language   | Channel                      | Metadata                                         |
| ---------- | ---------------------------- | ------------------------------------------------ |
| Python     | `PolymarketClient.channel()` | `PolymarketClient.metadata(participant_id=None)` |
| TypeScript | `PolymarketClient.channel()` | `PolymarketClient.metadata(participantId?)`      |
| Go         | `pmauth.Client.Channel()`    | `pmauth.Client.Metadata(participantID string)`   |
| bash       | no channel — `grpcurl` dials | `-H` flags, token from `token.sh`                |

The channel carries TLS and nothing else. **Authorization travels in the per-call metadata**, so one channel serves calls for many participants.

The metadata helper sets:

```
authorization: Bearer <access_token>
x-participant-id: <participantId>     # only when the call is account-scoped
```

It calls `access_token()` on every invocation, and `access_token()` re-mints at `expires_in` minus 30 seconds — so a long-lived channel never carries a stale token. Build metadata per call, never once at start-up.

`x-participant-id` is a per-call argument, not a client setting, because the same client makes calls at all three scopes: account-scoped reads **require** it, `CreateVendorOrder` does not use it, and `CashMovementService` **must not** receive it. Pass the participant ID only where the table on [Firms, participants and accounts](/identity) says to. Legacy drop-copy and balance-ledger examples that build metadata with only `authorization` return `403` as published.

The gRPC targets are the real ones — `grpc-api.preprod.polymarketexchange.com:443` and `grpc-api.prod.polymarketexchange.com:443`. The form `grpc-preprod.polymarketexchange.com` on the legacy environments page does not resolve.

### `curl` cannot speak gRPC

There is no REST equivalent for most of this surface, so the command-line tab on later pages is `grpcurl`, not `curl`. Two things it needs:

* **`-H` flags for the metadata**, one per header: `-H "authorization: Bearer ${TOKEN}"` and, on account-scoped calls, `-H "x-participant-id: ${PARTICIPANT_ID}"`.
* **`-import-path` and `-proto` pointing at your local proto copy.** Server reflection is entitlement-gated and returns `PermissionDenied: method not permitted` without the grant, so `-use-reflection` fails for most partners. See [Protos and SDKs](/environments#protos-and-sdks).

`token.sh` above is the token source: it prints the access token on stdout, so every `grpcurl` snippet in this space starts with `TOKEN="$(./token.sh)"`. It re-mints on every run, which is correct for a shell one-liner and wrong for a service — use one of the three long-running clients in a process that stays up.

```bash theme={null}
TOKEN="$(./token.sh)"    # the script above; stdout is exactly the token
SERVICE="..."            # fully-qualified service name, read from your own proto copy
METHOD="..."             # the RPC on it, read from your own proto copy

grpcurl \
  -import-path ./polymarket-protos \
  -proto polymarket/v1/order.proto \
  -H "authorization: Bearer ${TOKEN}" \
  -d '{}' \
  grpc-api.preprod.polymarketexchange.com:443 \
  "${SERVICE}/${METHOD}"
```

<Note>
  **\[VERIFY]** Fully-qualified service and method names are not published for most of this surface,
  and reflection cannot list them for you without the entitlement.

  Read them out of the proto copy your integration lead sent you.
</Note>

## Next

[Firms, participants and accounts](/identity)
