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.
{
"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.
{
"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).
{ "status": "unauthenticated" }GET /models
Requires an authenticated session; refreshes tokens, then lists the account's
model slugs. Disabled when enableResponsesProxy: false.
{ "models": ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"] }| Status | Body |
|---|---|
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):
| Header | Accepted values |
|---|---|
x-login-with-chatgpt-service-tier | auto, default, flex, priority, fast |
x-login-with-chatgpt-reasoning-effort | none, low, medium, high, xhigh |
| Status | Body / behavior |
|---|---|
200 | Upstream 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.
| Status | Body / behavior |
|---|---|
201 | WebRTC answer SDP. |
400 | invalid_realtime_request. |
401 | not_authenticated. |
403 | realtime_mode_not_allowed, realtime_transport_not_allowed, or origin_not_allowed. |
413 | realtime_request_too_large. |
upstream / 502 | realtime_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" }
}{
"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
allowedOriginspass. - Other origins get
403 { "error": "origin_not_allowed" }. - Requests without an
Originheader (curl, server-to-server) always pass; they cannot carry a browser's ambient cookie.
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.