Skip to main content
API reference: private_key_jwt flow · gRPC overview. Opens on the public documentation site in a new tab. Where it disagrees with this page, this page is authoritative for the partner surface.

Generate your keys

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.
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.
Then restrict the private key on disk:

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

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

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

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.
How the Client ID reaches you is being confirmed. 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.
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

There is no documented key rotation or revocation procedure. 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.
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

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

Token lifetime

expires_in comes back as 180.
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.
Cache one token per process and re-mint on demand. Do not mint a token per request.
The runnable client that implements this flow — and the channel and metadata helpers every later gRPC snippet imports — is on Canonical token client.

Scopes and entitlements

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

This is eight of eleven scopes. 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.

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.
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 — and ask for the reflection grant if you want it for debugging.

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

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

Check the header rule for that call

Look the call up in the x-participant-id table on Firms, participants and accounts.
  • 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.
3

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

What can go wrong

Next

Canonical token client