OAuth tokens are for the MCP server only. The REST API at
https://api.proxyjam.com/public/v1 keeps using API keys.Discovery
The issuer ishttps://proxyjam.com, exactly: no trailing slash and no path.
Clients compare it byte for byte with the iss on every redirect.
An MCP client does not need to know any of this in advance. Its first request
to the MCP server without a token gets:
https://mcp.proxyjam.com/mcp the
challenge names that host’s own resource metadata.
Agent-oriented summary: auth.md.
Endpoints
What the server supports:
- Grant types:
authorization_codeandrefresh_token. No implicit, password or client-credentials grant, and no OpenID Connect (noid_token). - PKCE: required,
S256only. - Resource indicators (RFC 8707): send
resourceon the authorize and token requests. Accepted values arehttps://proxyjam.com/mcp(the default whenresourceis absent),https://mcp.proxyjam.com/mcpandhttps://proxyjam.com. The access token’saudis that value. isson every redirect (RFC 9207), success and error alike.- Client authentication at the token endpoint:
none,client_secret_postorclient_secret_basic.private_key_jwtis not offered.
Registering a client
A client registers in one of two ways. Client ID Metadata Document. Theclient_id is an https URL whose JSON
document describes the client. ProxyJam fetches it when the client first sends
someone to the authorization endpoint. The document must list none among its
token endpoint authentication methods, must not declare a client secret, and
must serve its own URL as client_id. ProxyJam does not follow redirects when
it fetches the document and refuses private addresses. claude.ai, Claude Code
and ChatGPT all use this route, so they need no registration step.
Dynamic client registration.
client_id starting with dcr_, the metadata you
registered, and client_secret_expires_at: 0. A client registered with
client_secret_post or client_secret_basic also gets its client_secret,
shown once. A client registered this way is never shown as verified on the
consent screen, and one that is never used within 30 days is removed.
Redirect URIs
A redirect URI must be one of:httpswith a host, no fragment and no user info;httpon127.0.0.1,[::1]orlocalhost. The port is ignored when matching, sohttp://127.0.0.1/callbackcovershttp://127.0.0.1:8765/callback;- a private-use scheme in reverse-domain form, such as
com.example.app:/callback, for a client registered with"application_type": "native".
javascript: and data: included, is refused. A redirect URI
must match a registered one exactly, apart from the loopback port.
Signing in
-
Send the person’s browser to the authorization endpoint:
-
They sign in to ProxyJam if they are not already, and approve on the consent
screen at
app.proxyjam.com. The screen always shows where access will be sent. An app on the person’s own computer (a loopback redirect) gets a warning and is never marked as verified. -
The browser comes back to your redirect URI with
code,stateandiss=https://proxyjam.com, or witherror=access_deniedif they declined. An unknown client or a redirect URI that is not registered always ends on a ProxyJam error page, whatever the client — redirecting that kind of error back would make this an open redirector. Any other request error (no PKCE challenge, aresourcethis server does not serve, …) ends on a ProxyJam error page too, unless yours is a verified app or a loopback redirect, in which case it comes back aserror=on your redirect URI instead. -
Exchange the code within 60 seconds:
scopelists what the person actually granted, which can be less than you asked for. -
Call the MCP server with
Authorization: Bearer <access_token>.
Scopes
- Every read scope the client asks for is granted together. The person cannot withhold one and keep another.
- The spending box appears when the client asks for
orders:write. The MCP server’s challenge does, so every sign-in through it offers the box; it starts unticked. - Each approval replaces the scopes. Approving again without the box
removes
orders:writefrom the connection. All connections of one app to one account on the same MCP address share their scopes, and existing tokens pick up a change within 5 minutes. - Never granted: API-key management, email and password changes, account deletion, webhook management and wallet top-ups. No OAuth token reaches them, whatever it asks for.
- A tool outside the token’s scopes returns an error that names the missing scope. To allow spending, reconnect and tick the box.
Tokens and lifetimes
- Refresh tokens rotate. Use each one once. Presenting a refresh token that was already used revokes the whole connection, as does redeeming a code twice.
- A dead refresh token gets
invalid_grant; send the person through sign-in again. - A refresh may narrow
scopeto a subset of the connection’s scopes. It never widens it.
Revocation
- By the person. Settings → Connected apps in the dashboard lists every connection with its scopes and last use, and revokes any of them.
- By the client.
POST https://api.proxyjam.com/oauth/revokewithtoken(an access or refresh token) and the same client authentication as the token endpoint. Revoking either token revokes its whole connection. An unknown or expired token still gets200; a token issued to another client gets400 invalid_grant. - Automatically. Changing, resetting or first setting the password, signing out everywhere, and deleting the account revoke every connection. So does refresh-token or code reuse.
Errors
The token, registration and revocation endpoints answer errors as flat JSON:Agent registration
An agent that works for nobody in particular can still get a credential of its own, following auth.md (version 0.1, anonymous method). It is advertised in theagent_auth block of the authorization server
metadata.
Register
credential and claim_token are shown once. Send the key as
Authorization: Bearer pj_… (or X-API-Key) on the MCP server and on the REST
API. With catalog:read it reads the catalogue. It expires after 30 days unless
it is claimed.
Claim
A claim attaches the key to a person’s account, with that person’s consent.-
The agent asks the person for their email and posts it with the claim token:
The answer is
{"registration_id", "claim_attempt_id", "status": "initiated", "expires_at"}, the same whether or not the address has an account. - ProxyJam emails a link, valid for 60 minutes. On that page the person clicks Show my code and sees a 6-digit code, valid for 10 minutes and 5 tries. Opening the link again gives a fresh code. If the address already has a ProxyJam account, the page shows the code only to someone signed in to that account. Every claim page offers This wasn’t me, which cancels the claim.
-
The person reads the code to the agent, which completes the claim:
The answer is
{"registration_id": "reg_…", "status": "claimed"}. The same key keeps working, now with the read scopes, and no longer expires.
catalog:read, account:read, orders:read and
wallet:read. That includes your orders with their proxy logins and
passwords, your wallet balance and transactions, and your account details. A
claim never grants orders:write: a claimed key cannot spend money or
change a paid service. An existing account gets an email when a key is attached
to it, and the key is listed under API keys in the dashboard, where it can be
revoked.
An address with no account gets a new one, verified by the code and without a
password; set one later with Forgot password. While new registrations are
switched off, completing such a claim answers 403 registration_disabled.
A
401 on a key that used to work means it expired or was revoked: drop it and
register again.