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.
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.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: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,.pfxor.jksbundle — those contain the private key, - a signed client assertion as a “sample” — it is a live credential until it expires.
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.What we return
Your Client ID for that environment, plus your two firm names. The Client ID is theiss and sub of every client assertion you mint — see Authentication.
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
Two things you can do now that do not depend on us:- Keep the private key in a secret manager with access logging, so you can answer “who could have read this” without a rotation.
- 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 carriesauthorization: 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
- Build a JWT (the client assertion) and sign it with your private key, RS256.
POSTit to the Auth0 token endpoint withgrant_type=client_credentialsand theaudienceyou want the access token for.- Get back an access token with
expires_inof180seconds. - Send it as
authorization: Bearer <token>on REST calls and in gRPC metadata. - Re-mint when
expires_inminus 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.
Token lifetime
expires_in comes back as 180.
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.Three rules
- Scopes are granted server-side against your client. Ask your integration lead for the surfaces you need, per environment.
- Do not request scopes on the token exchange. Do not send a
scopeparameter to the token endpoint. Requesting a scope is not how you get it. - 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.
Known scopes
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 missingx-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.
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.