> ## 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.

# Authentication

> Generate and register your keypair, mint an access token with a signed client assertion, and understand the scopes and entitlements on that token.

<Info>
  **API reference:** <a href="https://docs.polymarket.us/trader-guide/authentication" target="_blank" rel="noreferrer">private\_key\_jwt flow</a> · <a href="https://docs.polymarket.us/streaming-endpoints/grpc-overview" target="_blank" rel="noreferrer">gRPC overview</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>

## Generate your keys

<Note>
  **If you are an IB, this differs.**
  There is no published key rotation or revocation procedure, and for you that is a **compliance
  exposure** rather than an operational gap — you are likely to be asked for your key lifecycle
  in the back and middle office overview. Record what you do and raise the gap in writing. See
  [Reporting pack](/regulatory#reporting-pack).
</Note>

Run this once for each environment, against that environment alone. You generate the keypair. We only ever see the public half. Run these commands before your credential gate, because we cannot issue a Client ID until we have a public key to register against it.

### Generate

Auth is **private\_key\_jwt** with an **RSA 2048** keypair. Name the files by firm and environment so the two pairs never get crossed.

```bash theme={null}
# Private key — stays on your infrastructure, forever.
openssl genrsa -out yourfirm-preprod-private.pem 2048

# Public key — this is the only file you send us.
openssl rsa -in yourfirm-preprod-private.pem -pubout -out yourfirm-preprod-public.pem
```

Then restrict the private key on disk:

```bash theme={null}
chmod 600 yourfirm-preprod-private.pem
```

### One keypair per environment

**Keys are per environment. Preprod credentials do not work in production.** Generate the production keypair separately, at the production credential gate, with its own filename:

```bash theme={null}
openssl genrsa -out yourfirm-prod-private.pem 2048
openssl rsa -in yourfirm-prod-private.pem -pubout -out yourfirm-prod-public.pem
chmod 600 yourfirm-prod-private.pem
```

Reusing the preprod private key in production means a preprod compromise is a production compromise, and there is no revocation procedure to fall back on — see [No rotation path exists](#no-rotation-path-exists).

### What to send us

Send **one file**: `yourfirm-<env>-public.pem`. It begins `-----BEGIN PUBLIC KEY-----`. Post it in your shared Slack channel, tagging the Polymarket side, and state the environment in the same message.

Never send:

* the private key (`-----BEGIN PRIVATE KEY-----` or `-----BEGIN RSA PRIVATE KEY-----`),
* a key passphrase,
* a `.p12`, `.pfx` or `.jks` bundle — those contain the private key,
* a signed client assertion as a "sample" — it is a live credential until it expires.

<Warning>
  **If a private key has ever left your infrastructure, treat it as compromised and generate a new
  pair immediately.** Tell us in the same message, because there is no self-service way to retire the
  old public key.
</Warning>

### Confirm the fingerprint

Compute the SHA-256 fingerprint of the public key locally and quote it when you send the file, so both sides can confirm we registered the key you meant to send.

```bash theme={null}
openssl rsa -pubin -in yourfirm-preprod-public.pem -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64
```

```
# Example output — a single base64 line
n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=
```

<Warning>
  **We do not yet publish a fingerprint confirmation step of our own.**

  Ask your integration lead to read the fingerprint back to you after registration. Until they do,
  you have no way to prove which key is live against your Client ID.
</Warning>

### What we return

Your **Client ID** for that environment, plus your two firm names. The Client ID is the `iss` and `sub` of every client assertion you mint — see [Authentication](#).

<Warning>
  **How the Client ID reaches you is being confirmed.**&#x20;
  Two delivery channels are in use today and we are consolidating to one. Ask your integration lead
  which one applies to you; do not sit waiting on the other.
</Warning>

Store the Client ID and the private key path in your secret manager, not in your repo. The Client ID is not a secret on its own, but pairing it with the private key is what mints tokens.

### No rotation path exists

<Warning>
  **There is no documented key rotation or revocation procedure.**&#x20;
  We cannot tell you today whether you can register a second public key to roll over without
  downtime, or how long retiring a key takes. Plan a rotation as a coordinated change with your
  integration lead, in a maintenance window, and give notice.
</Warning>

Two things you can do now that do not depend on us:

1. Keep the private key in a secret manager with access logging, so you can answer "who could have read this" without a rotation.
2. Keep the generation commands in a runbook with the fingerprint recorded, so a rotation is a 10-minute change on your side once we can accept it.

## The token exchange

With a keypair registered and a Client ID in hand, you can mint a token. Every REST call and every gRPC call carries `authorization: Bearer <access_token>`. You mint that token yourself with a signed client assertion; there is no password, no API secret and no login call.

### The flow

1. Build a JWT (the **client assertion**) and sign it with your private key, RS256.
2. `POST` it to the Auth0 token endpoint with `grant_type=client_credentials` and the `audience` you want the access token for.
3. Get back an access token with `expires_in` of `180` seconds.
4. Send it as `authorization: Bearer <token>` on REST calls and in gRPC metadata.
5. Re-mint when `expires_in` minus 30 seconds has elapsed.

### Client assertion claims

| Claim          | Value                             |
| -------------- | --------------------------------- |
| `alg` (header) | `RS256`                           |
| `iss`          | your Client ID                    |
| `sub`          | your Client ID                    |
| `aud`          | **the Auth0 token endpoint URL**  |
| `iat`          | now, in seconds                   |
| `exp`          | at most **5 minutes** after `iat` |
| `jti`          | unique per assertion              |

`jti` must be unique for every assertion you mint. Use a UUID or 16 random bytes; do not derive it from the timestamp alone, because two assertions minted in the same second will collide.

### The two audiences are different values

This is the most common authentication failure, and the legacy authentication page's `<Note>` conflates the two.

| Where                                              | Value                                                   | Preprod example                                |
| -------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------- |
| `aud` **inside the client assertion**              | The **Auth0 token endpoint**                            | `https://pmx-preprod.us.auth0.com/oauth/token` |
| `audience` **form parameter on the token request** | The **API base URL** you want the token to be valid for | `https://api.preprod.polymarketexchange.com`   |

**Wrong:** setting the assertion's `aud` to `https://api.preprod.polymarketexchange.com`. The token endpoint rejects the assertion, and the error names the assertion, not the audience, so it reads like a signing problem.

**Wrong:** sending `audience=https://pmx-preprod.us.auth0.com/oauth/token` on the token request. You get an access token whose audience is the authorization server, and every API call then fails authorization even though the token minted cleanly.

**Right:** assertion `aud` = token endpoint. Request `audience` = API base URL. The access token you get back has the API base URL as its audience.

<Warning>
  **Do not send `scope` on the token exchange.** Scopes are granted server-side against your client.
  Requesting them is not how you get them, and after we add a grant you must re-mint the token. See
  [Scopes and entitlements](#scopes-and-entitlements).
</Warning>

### Token lifetime

`expires_in` comes back as `180`.

<Warning>
  **Honour `expires_in` minus a 30-second buffer. Never hardcode 180.** Four legacy pages hardcode it.
  If the lifetime changes, a hardcoded client sends expired tokens and reads the resulting `401` as an
  outage.
</Warning>

Cache one token per process and re-mint on demand. Do not mint a token per request.

<Note>
  The runnable client that implements this flow — and the channel and metadata helpers
  every later gRPC snippet imports — is on
  [Canonical token client](/token-client).
</Note>

## Scopes and entitlements

<Note>
  **If you are an IB, this differs.**
  Ask for `read:reports` and `read:cash-movements` in your **initial** scope list. You need both
  from day one to build the reporting pack, and a scope added later means re-minting your token.
  See [Reporting pack](/regulatory#reporting-pack).
</Note>

Your token now mints; whether a call is permitted is a separate question. Scopes are granted server-side against your client. There is no request you can make that adds one, and a grant does not apply to a token you already hold.

### Three rules

1. **Scopes are granted server-side against your client.** Ask your integration lead for the surfaces you need, per environment.
2. **Do not request scopes on the token exchange.** Do not send a `scope` parameter to the token endpoint. Requesting a scope is not how you get it.
3. **Re-mint your token after a grant lands.** A token minted before the grant carries the old entitlement set for its full 180 seconds. Six of eight ISVs discovered their scope set by hitting `PERMISSION_DENIED`, and some of those calls would have worked on a fresh token.

Grants are per environment. A scope granted in preprod is not granted in production.

### Known scopes

| Scope                  | What it gates                                                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `read:instruments`     | Reference data reads, and `CreateInstrumentStateChangeSubscription` — that subscription needs **this scope only** and no `x-participant-id` |
| `read:marketdata`      | Market data reads and the market data stream                                                                                                |
| `read:l2marketdata`    | Level-2 depth. A separate grant from `read:marketdata`; holding one does not imply the other                                                |
| `read:positions`       | Position reads. Account-scoped, so these calls also need `x-participant-id`                                                                 |
| `read:reports`         | Report reads. Account-scoped, so these calls also need `x-participant-id`                                                                   |
| `read:cash-movements`  | Cash-movement reads. Firm-scoped — **do not** send `x-participant-id`                                                                       |
| `write:cash-movements` | `Transfer` and cash-movement creation. Firm-scoped                                                                                          |
| `kyc:write`            | KYC submission                                                                                                                              |

<Warning>
  **This is eight of eleven scopes.**&#x20;
  The canonical list of 11 scopes and the 40-row endpoint→scope table live on the legacy
  `/trader-guide/authentication` page and have not been migrated here. Ask your integration lead for
  the grant list on your client rather than inferring it from the names above.
</Warning>

### Entitlements are not scopes

Some surfaces are gated on a firm-level entitlement rather than a token scope. A token re-mint never fixes one of these; we have to enable it.

| Surface                 | Without the entitlement                                                 |
| ----------------------- | ----------------------------------------------------------------------- |
| gRPC server reflection  | `PermissionDenied: method not permitted`                                |
| `CheckoutAPI` / Aeropay | `403 method not permitted` — these are **not entitled for ISVs at all** |

<Note>
  **gRPC server reflection is entitlement-gated.** If `grpcurl` cannot list services, that is the
  entitlement, not a networking problem. Generate stubs from the proto bundle instead — see
  [Protos and SDKs](/environments#protos-and-sdks) — and ask for the reflection grant if you want it for debugging.
</Note>

### Telling the three failures apart

A missing scope, a missing `x-participant-id`, and a missing firm entitlement all surface as `PERMISSION_DENIED` or `403`. Run these three tests in order; each one costs a single call.

<Steps>
  <Step title="Re-mint the token and retry once">
    If the call now succeeds, the grant existed and your token predated it. **Cause: stale token.**
    Nothing to escalate.
  </Step>

  <Step title="Check the header rule for that call">
    Look the call up in the `x-participant-id` table on
    [Firms, participants and accounts](/identity).

    * Account-scoped and you sent no header → add it and retry. **Cause: missing header.**
    * Firm-scoped (`CashMovementService`) and you *did* send the header → remove it and retry.
      **Cause: header sent where it must not be.**

    Legacy drop-copy and balance-ledger examples build metadata with only `authorization` and return
    `403` as published, for exactly this reason.
  </Step>

  <Step title="Ask for the grant list">
    If a freshly minted token with the correct header still fails, it is a grant on our side —
    either a scope on your client or a firm entitlement. Post in your shared Slack channel with the
    full RPC name, the environment, and the verbatim error. There is no self-service view of your
    own grants.

    <Warning>
      **No self-service entitlement view exists.**

      Decoding your access token may show the grants as a claim, but the claim name is not published,
      so do not build alerting on it.
    </Warning>
  </Step>
</Steps>

## What can go wrong

| Where                   | Symptom                                                    | Cause                                                                                         | What you do                                                                         |
| ----------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Generate your keys      | Token endpoint rejects your assertion signature            | The private key does not match the public key we registered, usually preprod key against prod | Recompute the fingerprint and compare it with the one you sent for that environment |
| Generate your keys      | Token endpoint rejects a preprod-issued Client ID in prod  | Client IDs and keys are per environment                                                       | Use the production pair; see [Environments](/environments)                          |
| Generate your keys      | `openssl rsa -pubin` errors on your public file            | You exported the private key by mistake                                                       | Re-run the `-pubout` command; check the file starts `-----BEGIN PUBLIC KEY-----`    |
| Generate your keys      | You cannot tell which key is live                          | No fingerprint confirmation step is published                                                 | Ask your integration lead to read the fingerprint back                              |
| The token exchange      | Token endpoint rejects the assertion                       | Assertion `aud` set to the API base URL                                                       | Set `aud` to the token endpoint                                                     |
| The token exchange      | Token mints, every API call fails authorization            | `audience` on the token request set to the token endpoint                                     | Set `audience` to the API base URL                                                  |
| The token exchange      | Token endpoint rejects the signature                       | Private key does not match the public key registered for that environment                     | Compare fingerprints — [Generate your keys](#generate-your-keys)                    |
| The token exchange      | `importPKCS8` throws in Node                               | Your PEM is PKCS#1 (`BEGIN RSA PRIVATE KEY`)                                                  | `openssl pkcs8 -topk8 -nocrypt -in in.pem -out out.pem`                             |
| The token exchange      | Assertion rejected intermittently under load               | Duplicate `jti`, or `exp` more than 5 minutes after `iat`                                     | One fresh UUID per assertion; cap the TTL at 300 s                                  |
| The token exchange      | `401` every \~3 minutes                                    | Hardcoded 180-second lifetime with no buffer                                                  | Re-mint at `expires_in` minus 30 s                                                  |
| The token exchange      | `PERMISSION_DENIED` on a call that should work             | A scope grant landed after your current token was minted                                      | Re-mint, then read [Scopes and entitlements](#scopes-and-entitlements)              |
| The token exchange      | A preprod token returns `401` in production                | Tokens, keys and Client IDs are per environment                                               | Mint against the production token endpoint with the production pair                 |
| Scopes and entitlements | `PERMISSION_DENIED` on a call that worked for another firm | Scope not granted on your client                                                              | Ask for the grant, then re-mint                                                     |
| Scopes and entitlements | `PERMISSION_DENIED` right after we told you a grant landed | Token minted before the grant                                                                 | Re-mint; the old token stays valid for up to 180 s                                  |
| Scopes and entitlements | `PERMISSION_DENIED` naming a missing scope **\[VERIFY]**   | Scope not granted                                                                             | Ask for the grant, then re-mint                                                     |
| Scopes and entitlements | `PermissionDenied: method not permitted` from `grpcurl`    | Reflection entitlement not granted                                                            | Generate stubs from the bundle — [Protos and SDKs](/environments#protos-and-sdks)   |
| Scopes and entitlements | `403 method not permitted` on checkout or Aeropay          | Not entitled for ISVs                                                                         | Use wire deposits                                                                   |
| Scopes and entitlements | `403` on drop copy or the balance-ledger stream            | Metadata carried only `authorization`; both are account-scoped                                | Add `x-participant-id` — [Identity](/identity)                                      |
| Scopes and entitlements | `PERMISSION_DENIED` on cash movements with a valid scope   | You sent `x-participant-id` on a firm-scoped call                                             | Remove the header                                                                   |

## Next

[Canonical token client](/token-client)
