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.

POST /realtime

Experimentally exchanges a browser WebRTC offer through an undocumented ChatGPT web transport. The credential remains server-side. This is not the public Realtime API. The request is JSON:

{
  "sdp": "v=0\r\n…",
  "session": { "voice": "juniper", "transport": "wm", "voiceMode": "wingman" }
}

Success is 201 application/sdp. session accepts the complete schema in experimental ChatGPT voice transport.

StatusBody / behavior
201WebRTC answer SDP.
400invalid_realtime_request.
401not_authenticated.
403realtime_mode_not_allowed, realtime_transport_not_allowed, or origin_not_allowed.
413realtime_request_too_large.
upstream / 502realtime_request_failed, with bounded upstream detail.

App-server Realtime routes

These routes are present only when realtime.appServer is configured. They use the authenticated Codex session managed by the handler and require a Node/Bun server with the codex executable. They do not use realtime.getAuth.

POST /realtime/app-server

Starts native GPT Live audio plus desktop-style tool handoffs:

{
  "sdp": "v=0\r\n…",
  "session": { "voice": "vale", "model": "selected-model-slug" }
}
201
{
  "sessionId": "opaque-live-session-id",
  "sdp": "v=0\r\n…"
}

Only schemas configured in realtime.appServer.tools are exposed. The handler closes an older Live process owned by the same login session before starting a new one.

GET /realtime/app-server/:id/events

Streams application/x-ndjson events: session.started, session.closed, handoff, tool.running, tool.completed, tool.pending_confirmation, tool.failed, error, and keepalive. A pending-confirmation event contains the application's safe review payload, not credentials or raw tokens. Lifecycle events emitted before the browser opens the stream are queued in a bounded server-side buffer and delivered when it subscribes.

POST /realtime/app-server/:id/confirm

{
  "callId": "tool-call-id",
  "confirmation": { "approved": true }
}

The confirmation value is application-defined. The configured confirmTool must re-read the pending resource, verify ownership, enforce idempotency, and perform the action. Spoken approval is never substituted for this request.

DELETE /realtime/app-server/:id

Stops the WebRTC Realtime thread, terminates app-server, and removes temporary server credentials. Logout also closes every Live process owned by that login session.

CSRF rules

All non-GET routes, including direct and app-server Realtime routes, 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