Skip to main content
ProxyJam runs an OAuth 2.1 authorization server for its MCP server. It is what the Sign in button in claude.ai, Claude Code and ChatGPT talks to: the person approves a connection on a ProxyJam consent screen, and the MCP client gets a token for that person’s account without ever seeing a password or an API key.
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 is https://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:
and discovery follows from there. On 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_code and refresh_token. No implicit, password or client-credentials grant, and no OpenID Connect (no id_token).
  • PKCE: required, S256 only.
  • Resource indicators (RFC 8707): send resource on the authorize and token requests. Accepted values are https://proxyjam.com/mcp (the default when resource is absent), https://mcp.proxyjam.com/mcp and https://proxyjam.com. The access token’s aud is that value.
  • iss on every redirect (RFC 9207), success and error alike.
  • Client authentication at the token endpoint: none, client_secret_post or client_secret_basic. private_key_jwt is not offered.

Registering a client

A client registers in one of two ways. Client ID Metadata Document. The client_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.
The answer carries a 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:
  • https with a host, no fragment and no user info;
  • http on 127.0.0.1, [::1] or localhost. The port is ignored when matching, so http://127.0.0.1/callback covers http://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".
Everything else, javascript: and data: included, is refused. A redirect URI must match a registered one exactly, apart from the loopback port.

Signing in

  1. Send the person’s browser to the authorization endpoint:
  2. 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.
  3. The browser comes back to your redirect URI with code, state and iss=https://proxyjam.com, or with error=access_denied if 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, a resource this 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 as error= on your redirect URI instead.
  4. Exchange the code within 60 seconds:
    scope lists what the person actually granted, which can be less than you asked for.
  5. 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:write from 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 scope to 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/revoke with token (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 gets 200; a token issued to another client gets 400 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.
A revoked connection stops working within 5 minutes, and its refresh token is refused at once. Revocation is final: signing in again creates a new connection.

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 the agent_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.
  1. 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.
  2. 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.
  3. 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.
What a claim grants: 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.