OAuth for MCP clients
Reference for building an MCP client that signs users in to TwexAPI: discovery, dynamic client registration, PKCE, scopes, tokens, refresh, and revocation.
TwexAPI runs an OAuth 2.1 authorization server for its MCP server. MCP clients such as Claude, ChatGPT, and Cursor use it automatically. Read this page if you are building your own client or debugging one.
- Authorization code flow with PKCE (
S256only). - Public clients only: no client secret. Register with dynamic client registration (RFC 7591).
- Users sign in and approve on
https://twexapi.io/oauth/consentwith their TwexAPI account. - Tokens are opaque and work only on
https://api.twexapi.io/mcp. They are not TwexAPI API keys and cannot call the REST API. - Calls are billed to the user’s credits through their API key, so the user needs an active key with credits to approve.
If you only need a script or backend to call TwexAPI, use an API key instead.
Discovery
A request to https://api.twexapi.io/mcp without credentials returns 401 with:
WWW-Authenticate: Bearer realm="twexapi", resource_metadata="https://api.twexapi.io/.well-known/oauth-protected-resource/mcp", scope="mcp:read mcp:write"
Protected resource metadata
GET https://api.twexapi.io/.well-known/oauth-protected-resource/mcp (also served at /.well-known/oauth-protected-resource):
{
"resource": "https://api.twexapi.io/mcp",
"authorization_servers": ["https://api.twexapi.io"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write"],
"resource_name": "TwexAPI"
}
Authorization server metadata
GET https://api.twexapi.io/.well-known/oauth-authorization-server:
{
"issuer": "https://api.twexapi.io",
"authorization_endpoint": "https://api.twexapi.io/oauth/authorize",
"token_endpoint": "https://api.twexapi.io/oauth/token",
"registration_endpoint": "https://api.twexapi.io/oauth/register",
"revocation_endpoint": "https://api.twexapi.io/oauth/revoke",
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"revocation_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["mcp:read", "mcp:write"],
"authorization_response_iss_parameter_supported": true
}
Scopes
| Scope | Grants |
|---|---|
mcp:read |
explore and twexapi_request (read-only endpoints). Always granted. |
mcp:write |
twexapi_write (posting tweets and replies). The user can decline it on the consent page. |
Request mcp:read mcp:write to offer both. Unknown scopes are ignored, and a request with no recognized scope asks for both. The token response’s scope field tells you what the user granted.
1. Register the client
POST https://api.twexapi.io/oauth/register
Content-Type: application/json
{
"client_name": "My Agent",
"redirect_uris": ["http://127.0.0.1:53682/callback"]
}
{
"client_id": "twx_client_...",
"client_id_issued_at": 1792600000,
"client_name": "My Agent",
"redirect_uris": ["http://127.0.0.1:53682/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
| Field | Rules |
|---|---|
redirect_uris |
Required. 1 to 10 URIs, each one of: https://; http:// on localhost, 127.0.0.1, or [::1]; or a private-use scheme such as cursor://. No fragments or user info. |
client_name |
Optional, up to 100 characters. Shown on the consent page and in the user’s Authorized apps list. |
token_endpoint_auth_method |
Optional. Only none is accepted. |
grant_types |
Optional. A subset of authorization_code and refresh_token. |
response_types |
Optional. Only code. |
Registration is limited to 30 clients per IP address per hour. A client registration lasts 180 days and is extended every time a user starts an authorization with it, so register once and reuse the client_id.
2. Create a PKCE pair
Node.js:
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
Python:
import base64, hashlib, secrets
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
The verifier must be 43 to 128 characters from A-Z a-z 0-9 - . _ ~.
3. Send the user to authorize
Open this URL in the user’s browser:
https://api.twexapi.io/oauth/authorize
?response_type=code
&client_id=twx_client_...
&redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2Fcallback
&code_challenge=CHALLENGE
&code_challenge_method=S256
&state=RANDOM_STATE
&scope=mcp%3Aread%20mcp%3Awrite
&resource=https%3A%2F%2Fapi.twexapi.io%2Fmcp
| Parameter | Required | Notes |
|---|---|---|
response_type |
Yes | code. |
client_id |
Yes | From registration. |
redirect_uri |
Yes | Must match a registered URI exactly. Loopback http URIs may use any port. |
code_challenge, code_challenge_method |
Yes | S256 only. |
state |
Recommended | Returned unchanged; check it to prevent CSRF. |
scope |
No | See Scopes. |
resource |
No | If sent, must be https://api.twexapi.io/mcp. |
TwexAPI redirects the user to the consent page at twexapi.io. The user signs in, reviews the app name and the host they will return to, chooses whether to grant write access, and approves or denies. The request expires after 10 minutes.
4. Receive the code
On approval, the browser returns to your redirect URI:
http://127.0.0.1:53682/callback?code=CODE&state=RANDOM_STATE&iss=https%3A%2F%2Fapi.twexapi.io
Check that state matches and that iss is https://api.twexapi.io. On denial, the redirect carries error=access_denied instead of code. The code is single use and expires after 2 minutes.
If client_id is unknown or redirect_uri is not registered, TwexAPI shows a 400 error instead of redirecting, so a misconfigured client never sends users to an unverified address.
5. Exchange the code
POST https://api.twexapi.io/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=CODE
&redirect_uri=http%3A%2F%2F127.0.0.1%3A53682%2Fcallback
&client_id=twx_client_...
&code_verifier=VERIFIER
{
"access_token": "twx_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "twx_rt_...",
"scope": "mcp:read mcp:write"
}
redirect_uri and client_id must match the authorization request. You may also send resource=https://api.twexapi.io/mcp. A JSON body is accepted as well.
6. Call the MCP server
POST https://api.twexapi.io/mcp
Authorization: Bearer twx_at_...
Content-Type: application/json
Accept: application/json, text/event-stream
The access token works only on /mcp. An expired, revoked, or unknown token returns 401 with error="invalid_token" in the WWW-Authenticate header; refresh it or ask the user to sign in again.
Refresh tokens
POST https://api.twexapi.io/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=twx_rt_...
&client_id=twx_client_...
Each refresh returns a new access token and a new refresh token, and the old refresh token stops working. Store the new pair every time. If an old refresh token is used again, TwexAPI treats it as leaked and revokes the whole authorization, so every token from it stops working and the user has to sign in again.
Revocation
POST https://api.twexapi.io/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=twx_rt_...
Revoking a refresh token ends the whole authorization. Revoking an access token ends only that token. The endpoint always returns 200, even for unknown tokens. Users can also revoke any app under Authorized apps in the dashboard, which takes effect immediately.
Token lifetimes
| Item | Lifetime |
|---|---|
| Authorization request (consent page) | 10 minutes |
| Authorization code | 2 minutes, single use |
| Access token | 1 hour |
| Refresh token | 30 days, rotated on every use |
| Authorization not used for 30 days | Expires |
| Client registration | 180 days, extended on each authorization |
Errors
Errors use the standard OAuth shape:
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid, expired or already used"
}
| Endpoint | error |
Cause |
|---|---|---|
| Register | invalid_redirect_uri |
A redirect URI is missing, malformed, or uses a disallowed scheme or host. |
| Register | invalid_client_metadata |
Unsupported token_endpoint_auth_method, grant_types, or response_types. |
| Register | temporarily_unavailable (429) |
Too many registrations from your IP address. Reuse your client_id. |
| Authorize | invalid_client (400, no redirect) |
Unknown client_id. Register first. |
| Authorize | invalid_request (400, no redirect) |
redirect_uri is not registered for this client. |
| Authorize | unsupported_response_type |
response_type is not code. |
| Authorize | invalid_request |
Missing or invalid PKCE parameters. |
| Authorize | invalid_target |
resource is not https://api.twexapi.io/mcp. |
| Authorize | access_denied |
The user denied access. |
| Token | invalid_grant |
Code or refresh token is invalid, expired, already used, or issued to another client; PKCE verification failed; or the authorization was revoked. |
| Token | invalid_target |
resource does not match. |
| Token | unsupported_grant_type |
Use authorization_code or refresh_token. |
The consent page also stops the user before approval when their account has no usable API key or no credits left, so your client never receives a token that cannot make calls.