Login with ChatGPT
Reference

HTTP routes

Every route the handler serves, with request/response shapes, status codes, and CSRF rules.

All paths are relative to basePath (default /api/chatgpt). Responses are JSON unless noted. Unknown paths return a plain-text 404; a known path with the wrong method returns 405 with an Allow header.

POST /login

Starts a device login, or reuses the session's still-valid pending device code without calling upstream again. Sets the session cookie when none exists.

200
{
  "status": "pending",
  "userCode": "7B0J-DPK78",
  "verificationUrl": "https://auth.openai.com/codex/device",
  "interval": 5,
  "expiresAt": 1780000000000
}

GET /status

Advances the login by at most one upstream poll (respecting interval), then returns public state. While pending it returns the device fields above; token refresh also happens here for authenticated sessions near expiry.

200
{
  "status": "authenticated",
  "user": { "accountId": "acct_…", "email": "user@example.com", "plan": "pro" }
}

status is one of unauthenticated, pending, authenticated, expired, error.

GET /session

Same response shape as /status, but never polls upstream, so it is a cheap read for initial page loads.

POST /logout

Deletes the session from the store and clears the cookie (Max-Age=0).

200
{ "status": "unauthenticated" }

GET /models

Requires an authenticated session; refreshes tokens, then lists the account's model slugs. Disabled when enableResponsesProxy: false.

200
{ "models": ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"] }
StatusBody
401{ "error": "not_authenticated" }
upstream status or 502{ "models": [], "error": "<code>", "message": "…" }

POST /responses

The streaming proxy. Accepts a Responses-API JSON body, injects credentials, normalizes it for the Codex backend, and streams the upstream body back with cache-control: no-store. Disabled when enableResponsesProxy: false.

Request headers the proxy understands (details in Fast tier & thinking effort):

HeaderAccepted values
x-login-with-chatgpt-service-tierauto, default, flex, priority, fast
x-login-with-chatgpt-reasoning-effortnone, low, medium, high, xhigh
StatusBody / behavior
200Upstream stream, content type forwarded from upstream.
400{ "error": "invalid_responses_request", "message": "…" }, invalid_service_tier, or invalid_reasoning_effort.
401{ "error": "not_authenticated" } when there is no live session.
403{ "error": "model_not_allowed", "model": "…" } or { "error": "origin_not_allowed", … }.
413{ "error": "responses_request_too_large", "maxRequestBytes": … }.
429{ "error": "rate_limited", "retryAfterSeconds": … } plus a retry-after header.
upstream status{ "error": "responses_request_failed", "status": …, "detail": "…" }. The handler also logs the upstream status and body snippet.

When a fast service tier is rejected upstream, the handler retries once without it and adds x-login-with-chatgpt-service-tier-fallback: auto to the response.

CSRF rules

All non-GET routes (/login, /logout, /responses) check the Origin header on browser requests:

  • Same-origin requests and origins listed in allowedOrigins pass.
  • Other origins get 403 { "error": "origin_not_allowed" }.
  • Requests without an Origin header (curl, server-to-server) always pass; they cannot carry a browser's ambient cookie.

One cookie, lwc_session by default: an HMAC-signed random id, HttpOnly, SameSite=Lax, Path=/, Max-Age from sessionTtlMs, and Secure when the request is HTTPS (directly or via X-Forwarded-Proto). Attributes are configurable via the cookie option.

All error shapes, including the ones thrown as exceptions rather than returned as JSON, are collected in Error codes.

On this page