
# ChatGPT-Web2API
**Turn ChatGPT into an API. No API key. No token extraction. No sentinel solving.**
One command starts a Chrome browser, logs into ChatGPT, and exposes an OpenAI-compatible API + MCP server.
[](https://github.com/Elephant-Rock-Lab/ChatGPT-Web2API/actions/workflows/ci.yml)
[](https://github.com/Elephant-Rock-Lab/ChatGPT-Web2API/releases)
[](LICENSE)
[](https://www.python.org/downloads/)
[](https://github.com/Elephant-Rock-Lab/ChatGPT-Web2API/actions)
---
## Why This Exists
You have a **ChatGPT Plus subscription** but want to use it programmatically:
- **Build AI agents** that chat with ChatGPT via MCP (Model Context Protocol)
- **Use the OpenAI Python SDK** against your ChatGPT account — no separate API key
- **Access ChatGPT Projects** for persistent memory and custom instructions
- **Manage memories, conversations, and projects** from code
Other reverse proxies require token extraction, sentinel challenge solving, or cookie management. This one **drives a real Chrome browser** — anti-bot challenges (Turnstile, PoW) are handled automatically.
## Demo
```bash
$ pip install chatgpt-web2api
$ chatgpt-web2api
✓ Chrome launched on port 9222
✓ Navigating to chatgpt.com...
✓ Ready on http://localhost:8080
$ curl -s http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"What is 8+7?"}]}'
{"choices":[{"message":{"content":"8 + 7 = 15"},"finish_reason":"stop"}]}
```
```python
# OpenAI Python SDK — drop-in replacement
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
print(client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "What is 8+7?"}]
).choices[0].message.content)
# → "8 + 7 = 15"
```
## How It Works
```
┌──────────────┐ OpenAI API ┌──────────────┐ CDP ┌──────────────┐
│ Your code │ ──────────────► │ API Server │ ────────► │ Chrome │
│ SDK / curl │ ◄────────────── │ or MCP │ ◄──────── │ chatgpt.com │
│ MCP client │ JSON / SSE │ Server │ events │ (logged in) │
└──────────────┘ └──────────────┘ └──────────────┘
```
The proxy types messages, clicks send, and reads responses — exactly like a human. The browser handles all anti-bot challenges transparently.
## Quick Start
```bash
pip install chatgpt-web2api
# Start — opens Chrome, waits for login on first run
chatgpt-web2api
# Then use it:
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"What is 2+2?"}]}'
# → {"choices":[{"message":{"content":"4"}}]}
```
That's it. **One install, one command, one endpoint.**
## What You Get
### 🌐 OpenAI-Compatible REST API
Drop-in replacement for `api.openai.com`:
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Hello!"}],
stream=True,
)
for chunk in response:
print(chunk.choices[0].delta.content or "", end="")
```
### ⏱️ Rate Limits & Agent Retry
ChatGPT occasionally shows a "Too many requests" pop-up when conversations are
accessed rapidly. ChatGPT-Web2API turns this from a silent failure into a
**practical, agent-friendly signal** — so autonomous workflows back off and
recover instead of breaking.
**Transparent by default.** When the pop-up appears, the server dismisses it
(clicks "Got it") and retries your request with exponential backoff — up to
3 attempts. A *transient* limit is invisible: your agent gets a normal
successful response and never knows it happened.
**A clean pause signal when it persists.** If the limit survives all retries,
the server surfaces a **standard OpenAI `429`** that every major agent framework
already knows how to handle — with **zero client-side integration**:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{"error": {"type": "rate_limit_exceeded", "code": "rate_limit_exceeded", ...}}
```
The OpenAI SDK, LangChain, LlamaIndex, and similar frameworks auto-retry on
`429` + `Retry-After` by default. The wait (`Retry-After`) is parsed from the
pop-up text when ChatGPT gives an exact number, otherwise a conservative 60s.
**MCP clients** (Claude Desktop, Cursor) get the same signal semantically: a
`CallToolResult` with `isError=True` and a machine-readable `structuredContent`
payload an agent can read to decide "pause, then retry this tool":
```json
{"rate_limited": true, "retry_after": 60, "error": "rate_limit_exceeded"}
```
> **Streaming caveat:** a 429 can be sent *before* streaming begins (a pre-flight
> check), but a limit that appears *mid-stream* (after the 200 is committed) is
> surfaced as an inline `[Error: rate_limit_exceeded — retry in Ns]` chunk,
> since the HTTP status can no longer change. Non-streaming requests are
> unaffected.
### 🩺 Troubleshooting API/UI Drift
ChatGPT changes its web API over time, which can silently break driver functions
(the unit-test mocks can't detect this). ChatGPT-Web2API ships a **reactive
diagnostic** that captures evidence at the moment a function breaks, plus a
`doctor` command that auto-discovers the breakage and prints the evidence for
fast repair.
**Enable capture** (off by default) so breakage in the wild is recorded:
```bash
W2A_DIAGNOSE=1 chatgpt-web2api start
```
When a function returns a broken shape, a redacted artifact is written to
`~/.chatgpt-web2api/diagnostics/--.json` with the
exact request, live response, expected-vs-actual mismatch (secrets stripped).
**Auto-discover + diagnose:**
```bash
chatgpt-web2api doctor # which functions have captured breakage?
chatgpt-web2api doctor create_project# print the captured evidence for one
chatgpt-web2api doctor --verify get_models # re-run a read function live to test a fix
```
`doctor` (no args) **auto-discovers** which functions are broken — no human
needs to name them. It then prints the evidence an AI coding agent (or you)
reads to propose the corrected payload/selector/parse. `--verify` confirms a
read-path fix against the live account before you commit it. No AI is bundled —
the project captures deterministic evidence; the fix is human-applied.
### 🤖 MCP Server (16 Tools for AI Agents)
Expose ChatGPT to Claude Desktop, Cursor, Craft Agent, or any MCP client. Two
transports are supported — **SSE is recommended** for persistent clients like
ZCode; stdio is kept as a compatibility/dev mode.
#### SSE transport (persistent server; single active session until B1)
SSE is useful for one long-lived MCP server, but **current master does not yet
provide per-client tab/session isolation inside one SSE process**. Multiple MCP
clients may attach to the same SSE endpoint, but they share one MCP server, one
`CDPDriver`, one Chrome tab, and one browser conversation surface. Running
parallel agent sessions through the same SSE server can cause cross-session
interference: one session may navigate, continue, or mutate the browser state
expected by another.
Until the B1 MCP session-affine tab pool is implemented and enabled, use SSE
for a **single active agent session at a time**. For parallel agent sessions
today, use separate MCP processes with `parallel_tabs=true` and
`tab_mode=owned`, and ensure the Chrome-owning `chatgpt-web2api` REST process
is started with the same parallel config or is not serving REST traffic on
that CDP port. Otherwise, wait for B1.
Start it (in a dedicated terminal — Chrome must already be running with an
authenticated session, i.e. `chatgpt-web2api` started first):
```bash
chatgpt-web2api-mcp --transport sse --port 8090
```
Then add the client config:
```json
{
"mcpServers": {
"chatgpt-web2api-sse": {
"type": "sse",
"url": "http://localhost:8090/sse"
}
}
}
```
Every client session that attaches to that URL currently shares the single MCP
server and Chrome connection. This avoids per-session process growth, but it is
**not safe for parallel independent agent sessions** until B1's session-affine
driver/tab pool is available and explicitly enabled.
> **Multi-session warning:** A2 prevents stale-return by anchoring responses to
> the submitted turn, but it does not isolate multiple MCP SSE clients from each
> other. Shared SSE remains a single browser-control surface today.
#### Alternative: stdio transport (per-session, dev/debug)
Stdio spawns one MCP child process per client session, so it is not
recommended for many concurrent ZCode sessions. It is useful for single-client
setups, local development, and debugging (logs go straight to stderr):
```json
{
"mcpServers": {
"chatgpt": {
"command": "chatgpt-web2api-mcp",
"args": ["--cdp-port", "9222"]
}
}
}
```
**16 tools** give the agent full control:
| Tool | What it does |
|------|-------------|
| `chat_completion` | Send message, get response (multi-turn, streaming) |
| `list_models` | 17 live model slugs (GPT-5.5, 5.4 Thinking, etc.) |
| `list_projects` | ChatGPT projects with persistent memory |
| `create_project` | Create isolated workspace with dedicated memory |
| `delete_project` | Permanently remove a project (gated: `W2A_ENABLE_DESTRUCTIVE`) |
| `list_conversations` | Recent chats with pagination |
| `get_conversation` | Full message history |
| `archive_conversation` | Archive/unarchive (reversible) |
| `delete_conversation` | Delete permanently |
| `list_memories` | Facts ChatGPT remembers (41 found in testing) |
| `create_memory` | Tell ChatGPT to remember something |
| `delete_memory` | Remove a memory |
| `list_gpts` | Discover Custom GPTs |
| `chat_with_gpt` | Chat with a specific Custom GPT |
| `update_project_instructions` | Change project system prompt |
| `list_project_files` | Files in a project's knowledge base |
Every tool includes rich descriptions with domain knowledge, Pydantic-validated input, structured output schemas, and proper `ToolAnnotations` — agents understand *how* and *when* to use each one without prompting.
### 🔒 MCP Tool Access Control
The MCP server has **no authentication of its own** — any client that can reach it can call its tools. Since several tools mutate or delete data on your *real* ChatGPT account, tools are gated by risk tier so the dangerous ones are never exposed by accident:
| Tier | Tools | Exposed when |
|------|-------|--------------|
| **Safe** (always visible) | `chat_completion`, `chat_with_gpt`, `list_models`, `list_projects`, `list_conversations`, `get_conversation`, `list_memories`, `list_gpts`, `list_project_files` | Out of the box |
| **Write** (account mutation) | `create_project`, `update_project_instructions`, `create_memory`, `archive_conversation` | `W2A_ENABLE_WRITE=1` |
| **Destructive** (irreversible) | `delete_conversation`, `delete_memory` | `W2A_ENABLE_DESTRUCTIVE=1` |
```bash
# Default: safe reads + core chat only — nothing can mutate your account
chatgpt-web2api-mcp
# Enable project/memory/archive creation
W2A_ENABLE_WRITE=1 chatgpt-web2api-mcp
# Full surface (also exposes deletes)
W2A_ENABLE_WRITE=1 W2A_ENABLE_DESTRUCTIVE=1 chatgpt-web2api-mcp
```
Gated tools are **hidden** from `list_tools` *and* **refused at call time** — a client that calls a gated tool by name gets an error result rather than silent execution. Binding the SSE transport to a non-loopback address with no `api_keys` configured logs a prominent warning.
### 🔌 ZCode Hook: One-Line Bootstrap with `ensure`
Instead of manually starting REST and SSE in separate terminals, run a single
command that reconciles both and exits — ideal for a ZCode session-start hook:
```bash
chatgpt-web2api ensure
```
`ensure` is **point-in-time reconciliation**, not a supervisor: it checks REST
(`/health`) and SSE, starts whichever is missing, verifies SSE with a real MCP
handshake, and exits `0` when both are ready (nonzero with a diagnostic if not).
If a service dies later, re-run `ensure` from the next hook/session.
```bash
# Reconcile REST on 8080 + SSE on 8090 (defaults)
chatgpt-web2api ensure
# Custom ports / config (propagated to the child processes)
chatgpt-web2api ensure --rest-port 8081 --mcp-sse-port 8091 --config ~/.my-config.json
```
Degraded-REST policy: `ensure` does **not** restart REST immediately on
`degraded` (Chrome alive, driver disconnected) — it polls for up to 20s first,
since that may be a transient CDP reconnect. Only `missing` or `broken` trigger
an immediate (re)start. This avoids a destructive Chrome bounce on transients.
### 🔧 Three Interfaces, One Chrome Session
```bash
# Terminal 1: Start Chrome + API
chatgpt-web2api
# Terminal 2: MCP server (SSE recommended for ZCode; stdio also supported)
chatgpt-web2api-mcp --transport sse --port 8090
# or: chatgpt-web2api-mcp # stdio (one MCP child per client)
# Terminal 3: Any OpenAI SDK, curl, or HTTP client
curl http://localhost:8080/v1/chat/completions ...
```
## What Makes This Different
| | ChatGPT-Web2API | chat2api | ChatGPTReversed | Official API |
|---|---|---|---|---|
| **Anti-bot handling** | ✅ Automatic (CDP) | ❌ Manual (PoW/Turnstile) | ❌ Manual | N/A |
| **Token extraction** | ❌ None needed | ✅ Required | ✅ Required | N/A |
| **Project memory** | ✅ Full CRUD | ❌ | ❌ | ❌ |
| **MCP server** | ✅ 16 tools | ❌ | ❌ | ❌ |
| **ChatGPT memories** | ✅ List/create/delete | ❌ | ❌ | ❌ |
| **Custom GPTs** | ✅ Chat with any GPT | ❌ | ❌ | ❌ |
| **OpenAI SDK compat** | ✅ Drop-in | ✅ | ❌ | ✅ (native) |
| **Streaming** | ✅ SSE | ✅ | ❌ | ✅ |
| **Multi-turn** | ✅ Auto-continue | ✅ | ❌ | ✅ |
| **Installation** | `pip install` | Docker | npm | `pip install openai` |
| **Cost** | Free (uses your subscription) | Free | Free | Pay-per-token |
**Key insight**: Other proxies fight the anti-bot system. This one *uses the browser as the solution* — Chrome handles Turnstile, PoW, and session management automatically.
## MCP Tools in Action
An AI agent connected via MCP can:
```python
# Create an isolated project with dedicated memory
create_project(name="Python Async Specialist", memory_scope="project_v2")
# Chat within the project (persistent memory across conversations)
chat_completion(message="What are Python coroutines?", project_id="g-p-abc123")
# Manage memories
list_memories() # → 41 facts ChatGPT remembers
create_memory(content="Always use type hints in Python code")
delete_memory(memory_id="abc-123")
# List and manage conversations
list_conversations(limit=10)
archive_conversation(conversation_id="xyz", archive=True)
# Chat with a Custom GPT
chat_with_gpt(gpt_id="g-hkJGhxxx", message="Analyze this data")
```
## Configuration
### CLI Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--port` | 8080 | API server port |
| `--cdp-port` | 9222 | Chrome debugging port |
| `--headless` | false | Run Chrome headless (may trigger detection) |
| `--log-level` | INFO | DEBUG, INFO, WARNING, ERROR |
### Config File (`config.json`)
```json
{
"port": 8080,
"host": "127.0.0.1",
"cdp_port": 9222,
"headless": false,
"default_model": "auto",
"default_project_id": null,
"tab_mode": "owned",
"parallel_tabs": false,
"api_keys": [],
"request_timeout": 120
}
```
See [`config.example.json`](config.example.json) for all available keys.
### MCP Session Pool (B1 — experimental, default off)
When `mcp_session_pool_enabled=true`, the MCP SSE server does **not** connect to
Chrome at startup. Instead, each MCP client session gets its own owned
`CDPDriver`/Chrome tab on first request (lazy materialization). Different
sessions get different tabs, capped by pool size. This enables parallel
multi-session use of one shared SSE endpoint.
Requires `parallel_tabs=true` + `tab_mode=owned` (validated at load).
```json
{
"parallel_tabs": true,
"tab_mode": "owned",
"mcp_session_pool_enabled": true,
"mcp_session_pool_size": 2
}
```
Or via environment:
```bash
W2A_PARALLEL_TABS=1 \
W2A_MCP_SESSION_POOL_ENABLED=1 \
W2A_MCP_SESSION_POOL_SIZE=2 \
chatgpt-web2api-mcp --transport sse --port 8090
```
Pool defaults are conservative (size=2, TTL=30min). Raise the size only after
the account-level burst canary shows no token-rotation errors or hard account
challenges from genuinely simultaneous multi-tab use.
### Environment Variables
```bash
W2A_PORT=8080 W2A_CDP_PORT=9222 chatgpt-web2api
```
See [`.env.example`](.env.example) for all available variables.
## Tested Models
Live models from a ChatGPT Plus account (June 2025):
| Slug | Type | Best For |
|------|------|----------|
| `auto` | Reasoning | General use (default) |
| `gpt-5-5` | Reasoning, 34K context | Complex analysis |
| `gpt-5-4-thinking` | Extended reasoning | Step-by-step logic |
| `gpt-5-3-mini` | Fast, 34K context | Simple tasks, speed |
| `gpt-5-mini` | Fast, 8K context | Quick answers |
Use `list_models` to get the current catalog.
## Performance
| Operation | Time |
|-----------|------|
| Simple question ("2+2?") | 7s |
| Complex reasoning | 15–36s |
| Multi-turn follow-up | 3–6s faster (auto-continue) |
| Model listing | <1s |
| Memory listing | <1s |
## Requirements
- Python 3.11+
- Chrome or Chromium installed
- ChatGPT Plus subscription
## Project Structure
```
src/chatgpt_web2api/
├── __main__.py CLI entrypoint (chatgpt-web2api)
├── config.py Configuration from file/env/CLI
├── chrome.py Chrome subprocess lifecycle
├── cdp_driver.py CDP primitives (24 methods)
├── api_server.py OpenAI-compatible HTTP server
├── mcp_server.py MCP server (16 tools, resources, prompts)
└── service.py Orchestrator: Chrome → CDP → API/MCP
```
## Testing
The suite has two tiers:
**Unit tests (default, CI)** — fast, no browser, fully mocked. Run with `pytest`.
These cover structure, business logic, MCP protocol-level behavior, and access gating.
```bash
pytest -v # all unit tests (CI runs: pytest -m "not e2e")
```
**End-to-end tests (opt-in)** — drive a *real* ChatGPT account via Chrome CDP.
These are the only tests that exercise the driver↔ChatGPT boundary, which is
exactly where latent bugs hide (the E2E suite has already found four). They
mutate the account, so they are **deselected by default** and never run in CI.
```bash
# 1. Launch Chrome with CDP (reuses your logged-in profile)
# "chrome.exe" --remote-debugging-port=9222 \
# --user-data-dir=~/.chatgpt-web2api/chrome-profile https://chatgpt.com/
# 2. Run the E2E suite
W2A_E2E_RUN=1 pytest -m e2e -v
```
E2E safety model:
- **Snapshot-before / diff-after** for every destructive op — no test ever
deletes state it didn't create in the same session.
- A session registry + **guaranteed-cleanup finalizer** removes anything a test
created, even on crash.
- **Pacing** (`W2A_E2E_PACE`, default 12s) between tests to avoid ChatGPT's
"Too many requests" rate limit.
- Reads are zero-risk; chat/memory operations create throwaway state that's
cleaned up; `create_project`/`update_project_instructions` are `xfail` (broken
against the current live API — see CHANGELOG).
If not logged in, the suite opens a headed window and waits up to 10 minutes
for you to log in.
## Documentation
**[📋 Full documentation index](docs/INDEX.md)** — start here if you're unsure which doc to read.
Key docs:
- [Deployment Guide](docs/deployment.md) — pip install, Docker, cookie injection, multi-instance, sharing
- [OS Supervision Guide](docs/os-supervision.md) — systemd / launchd / Task Scheduler / NSSM for always-on deployments
- [Production Runbook](docs/runbook.md) — health interpretation, failure modes, breaker states, auth recovery, safe restart
- [API Reference](docs/api-reference.md) — OpenAI-compatible REST endpoints + MCP server surface
- [Architecture](docs/architecture.md) — codebase structure and the CDPDriver hub-and-spoke module layout
- [Protocol Reference](docs/protocol-reference.md) — captured ChatGPT web API endpoints
- [Roadmap](docs/ROADMAP.md) — phases 1–6, what landed, what's deferred
- [Contributing](CONTRIBUTING.md) — how to contribute
- [Changelog](CHANGELOG.md) — version history
## Limitations
- **Single browser session** — one Chrome profile = one ChatGPT account (scale with nginx round-robin)
- **No headless** — headless Chrome triggers ChatGPT's bot detection; use VNC on servers
- **Cookie expiry** — auth cookies expire ~2 weeks; re-login needed
- **Serial requests** — one chat at a time through the browser *by default* (concurrent reads are fine). For per-tab parallelism on one shared Chrome, set `parallel_tabs: true` (requires `tab_mode: "owned"`); see [docs/deployment.md](docs/deployment.md) → "Parallel mode (one Chrome, many tabs)".
- **Memory writes** — ChatGPT's `/backend-api/memories` is read-only; creating memories works via chat interface
- **No image input** — text only (CDP file upload not yet implemented)
## Roadmap
- [ ] Image/file upload to conversations via CDP drag-and-drop
- [ ] Headless mode with anti-detection patches
- [ ] Concurrent chat pooling across multiple Chrome instances (per-tab parallelism on one Chrome landed via `parallel_tabs`; the cross-instance pool/router is still future work)
- [ ] Web search mode (trigger ChatGPT's built-in search)
- [ ] DALL-E image generation via ChatGPT
- [ ] Canvas/code execution support
- [ ] Retry logic with exponential backoff for Chrome failures
## License
[MIT](LICENSE) © Elephant Rock Lab