Looking for a different doc? See the documentation index for a "which doc should I read?" routing table.
Three ways to share this with others, depending on the audience.
Anyone with Python and Chrome can run it in 3 steps:
# 1. Install
pip install git+https://github.com/your-org/ChatGPT-Web2API.git
# 2. Start — launches Chrome, opens chatgpt.com
chatgpt-web2api
# 3. First time: log into ChatGPT in the Chrome window that opens.
# The proxy detects login automatically and starts serving.The Chrome profile is saved at ~/.chatgpt-web2api/chrome-profile/. Subsequent starts skip login.
Create ~/.chatgpt-web2api/config.json:
{
"port": 8080,
"cdp_port": 9222,
"api_keys": ["sk-my-secret-key"],
"default_model": "auto"
}Or use environment variables:
W2A_PORT=9090 W2A_API_KEYS=sk-key1,sk-key2 chatgpt-web2apiRequires exporting cookies from an already-logged-in browser session.
Use a browser extension like EditThisCookie or Cookie-Editor:
- Open
chatgpt.comwhile logged in - Export all cookies for
chatgpt.comas JSON - Save as
cookies.json
The file should look like:
[
{
"name": "__Secure-next-auth.session-token",
"value": "...",
"domain": ".chatgpt.com",
"path": "/",
"secure": true,
"httpOnly": true
},
...
]# Build
docker build -t chatgpt-web2api .
# Run (mount cookies + persistent profile)
docker run -d \
--name chatgpt-proxy \
-p 8080:8080 \
-v ./cookies.json:/data/cookies/cookies.json:ro \
-v chatgpt-profile:/data/chrome-profile \
chatgpt-web2apicurl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"Hello"}]}'Cookies expire. When auth fails:
- Re-export fresh cookies from your browser
- Replace
cookies.json - Restart the container:
docker restart chatgpt-proxy
Run the proxy on a server, let others connect to it.
# On the server (with API key protection)
chatgpt-web2api --host 0.0.0.0 --port 8080
# Or with config
cat > config.json << 'EOF'
{
"port": 8080,
"host": "0.0.0.0",
"api_keys": ["sk-team-key-1", "sk-team-key-2"],
"default_model": "auto"
}
EOF
chatgpt-web2api --config config.jsonOthers connect:
from openai import OpenAI
client = OpenAI(
base_url="http://your-server:8080/v1",
api_key="sk-team-key-1"
)
resp = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Hello"}]
)Important: One mutating request at a time per tab by default. The browser is single-threaded per page. For team use, queue requests, run multiple instances on different ports, or enable parallel_tabs (see Parallel mode below) for per-tab concurrency on one shared Chrome.
To run the proxy continuously under an OS supervisor (survives reboots, restarts on failure), see the dedicated OS-Level Supervision Guide. It covers systemd (Linux), launchd (macOS), and Task Scheduler / NSSM (Windows), with two supervisor styles:
ensureon a timer — mirrors the ZCode hook model; reconciles REST + SSE on a schedule, no long-lived Python process.start+chatgpt-web2api-mcpas services — classic always-on; the supervisor owns restart policy for the two long-lived processes (REST owns Chrome; MCP/SSE is a separate process that attaches to it).
The guide is documentation only — no supervisor scripts are installed by the
package. ZCode users should prefer the ensure hook over OS supervision.
For interpreting /health, diagnosing failure modes, understanding breaker
states and cooldowns, recovering auth, collecting logs, and safe restart
sequences, see the dedicated Production Runbook. It covers:
- startup checklist + the reconcile command and its exit codes (0/1/2)
/healthfield reference and the exact conditions for eachstatus(starting/healthy/degraded/broken)- common failure modes mapped to symptoms and fixes
- the four breakers (
auth_required,composer_send_readiness,cdp_reconnect,chrome_crash_loop) with their thresholds/cooldowns - the auth-recovery flow (the one breaker that needs a human)
- the safe restart rule (restart on process exit, not on
degraded) - post-deploy validation including the exact-output
"Reply with exactly: ok"sanity send
All field names, state values, and thresholds are source-cited.
- Install EditThisCookie
- Go to
chatgpt.comwhile logged in - Click the cookie icon in toolbar
- Click "Export" → copies JSON to clipboard
- Save as
cookies.json
- Install Cookie-Editor
- Go to
chatgpt.comwhile logged in - Click the cookie icon → "Export" → "Export as JSON"
- Save as
cookies.json
- Open
chatgpt.com, press F12 → Console - Run:
document.cookie - Parse the cookie string into JSON format
Each Chrome instance handles one request at a time. Run multiple for throughput:
# Instance 1
chatgpt-web2api --port 8081 --cdp-port 9222
# Instance 2
chatgpt-web2api --port 8082 --cdp-port 9223 --user-data-dir ~/.chatgpt-web2api/chrome-profile-2
# Instance 3
chatgpt-web2api --port 8083 --cdp-port 9224 --user-data-dir ~/.chatgpt-web2api/chrome-profile-3Put a reverse proxy (nginx, Caddy) in front with round-robin:
upstream chatgpt {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 8080;
location / {
proxy_pass http://chatgpt;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
}
}parallel_tabs: true (or W2A_PARALLEL_TABS=1) lets several bridge processes
share one Chrome instance, each driving its own owned tab in parallel — no
proxy, no second browser profile. Per-target locking serializes same-tab
mutations while different tabs run concurrently. Requires tab_mode: "owned"
(enforced at load; the default).
# Process A — its own REST port, own tab on the shared Chrome (cdp 9222)
W2A_PARALLEL_TABS=1 chatgpt-web2api --port 8081 --cdp-port 9222
# Process B — different REST port, different tab on the SAME Chrome
W2A_PARALLEL_TABS=1 chatgpt-web2api --port 8082 --cdp-port 9222Requirements and caveats:
parallel_tabs: truerequirestab_mode: "owned"(enforced at load). Each process drives exactly one owned tab; mutating operations serialize per owned target, not globally per CDP port. This is not an in-process tab pool or request router — that remains future work.- Each process still needs its own local REST/MCP port (
--port). There is no single-endpoint router yet. - One ChatGPT account is shared across all tabs (one Chrome profile). Parallel mode increases browser-tab parallelism, not account quota — all workers share one session and may hit account-level rate limits; rate-limit handling stays reactive (backoff/retry).
- Unique instance identity per process. REST derives it from
rest:{port}automatically. MCP derives it frommcp:sse:{host}:{port}(SSE) ormcp:stdio:{pid}(stdio) whenparallel_tabs=true. The stdio PID identity is unique per run but not stable across restart (a new PID can't reclaim the prior tab); setW2A_INSTANCE_IDto a stable, unique-per-worker value if you need restart-reclaim. Do not reuse the sameW2A_INSTANCE_IDacross live workers on the same Chrome — that makes them collide on one tab. - Fail-closed, not fallback. If a process can't obtain (or loses mid-flight)
an owned tab, the operation fails retryably (REST 503
code=owned_tab_required/ MCPisError) rather than silently sharing a tab. Mixing per-target and port-wide locks would reintroduce split-brain. Other retryable 503/isErrorcodes (lock_timeout,circuit_open) keep their existing semantics.
Rollout warning: do not mix old (port-lock-only) workers and new (
parallel_tabs=true) workers on the same CDP port during a rolling upgrade — their lock files don't exclude each other. Finish the rollout on a port before enabling parallel mode on it.
The bridge creates owned tabs (tab_mode="owned", the default) — each
driver gets its own dedicated tab via Target.createTarget and its own
websocket. A Page.navigate issued by one CDP client goes over that client's
websocket to that client's tab; it cannot navigate another client's tab.
CDP targets are independent.
However, running multiple CDP clients (e.g., 4 Super-Browser MCP servers + the bridge + the MCP server = 6+ clients) on one Chrome instance can still degrade reliability through resource contention:
- CPU/memory saturation — each CDP client drives React hydration, DOM polling, and JS evaluation. Under load, the Chrome renderer can slow enough that ChatGPT's SPA fails to hydrate the composer within the bridge's readiness window (see P2 staged readiness diagnostics).
- CDP HTTP endpoint queuing — the
/json/listand/json/versionendpoints are served by Chrome's DevTools HTTP server, which is single-threaded. Many clients polling concurrently can introduce latency. - Profile-level state — all tabs share one Chrome profile (one account, one cookie jar, one service worker). A service-worker update or auth redirect on one tab can briefly affect the others.
What this means in practice: the bridge's write path (navigate + send)
is more sensitive to these effects than the read path (list/get
conversations via /backend-api/*), because the write path requires
DOM-level readiness (composer hydrated, send button enabled) while the read
path is a pure HTTP fetch. If you observe "navigation timeout" or "no send
button" errors that worsen as more clients connect, consider:
- Running the bridge on a dedicated Chrome instance (separate
--user-data-dirand--cdp-port). - Reducing the number of concurrent CDP clients on the shared Chrome.
- Enabling
parallel_tabs: trueso the bridge and MCP server each get their own tab (they do by default withtab_mode="owned", butparallel_tabsadds per-target locking and fail-closed semantics).
The bridge's error messages now include staged diagnostics (P2) that name the failing readiness stage, making this degradation diagnosable rather than opaque.
| Problem | Fix |
|---|---|
| "No access token" | Log into ChatGPT in the Chrome window that opens |
| "Chrome CDP did not respond" | Chrome isn't running. Check chrome_path in config |
| "Timed out waiting for assistant" | Page may be stuck. Restart the proxy |
| Cookies not working | Re-export fresh cookies. They expire every ~2 weeks |
| Headless fails | Anti-bot detection blocks headless. Use cookie injection + headed mode on a VNC/display. The Dockerfile ships W2A_HEADLESS=false for this reason; set W2A_HEADLESS=true only if you accept the anti-bot risk |