# Migrating from the legacy Crawl4AI Cloud API to the new one > For a coding agent. The legacy service, until launch at `api.crawl4ai.com` (the "v1" API with sessions, scan, site, > enrich, context, profiles) continues for a while at `https://api-old.crawl4ai.com`. The new API runs at > `https://api.crawl4ai.com` (also `https://api.crawl4ai.com`). Read this, rewrite every call in the > user's code base, run their tests, and show the diff. Full reference of the new API: https://crawl4ai.com/llms.txt ## What changes for every call - Base URL: `https://api.crawl4ai.com` (was `https://api.crawl4ai.com/v1/...`). No `/v1` prefix. - Auth: `Authorization: Bearer sk_live_...` (was the `X-API-Key` header). `x-api-key: sk_live_...` also works. - Keys: a new key from https://crawl4ai.com/ (the legacy keys do not work on the new API). - Responses are JSON; errors are `{"error": "..."}` with an HTTP status; `429` carries `X-RateLimit-*` headers; `402 no_credit` means the account is out of credits (every response carries `x-c4-cost` and `x-c4-balance`; `POST /v1/estimate` prices a call first). - No async job objects for single calls: `/scrape`, `/search`, `/extract` answer in one round trip. - The nearest region answers (EU · Singapore · US); pin one with `eu-gate` / `sin-gate` / `us-gate.crawl4ai.com`. ## Endpoint by endpoint | legacy (api-old) | new | how to rewrite | |---|---|---| | `POST /v1/scrape {url}` | `POST /scrape {url, format}` | `format`: `md` (default `both` = markdown + html). Response: `markdown`, `html`, `title`, `parse.*` when asked. `bypass_cache: true` stays in the body. | | `POST /v1/scrape/async {urls[]}` (a job, then poll) | `POST /scrape/batch {urls[], format}` (streamed) | one call, one result line per URL as it lands (NDJSON, `curl -N`); no polling. Thousands of URLs: `POST /scrape/jobs` (a job id, `GET /scrape/jobs/{id}`). | | `POST /v1/screenshot {url}` / `{url, mode: "pdf"}` | not offered | keep the legacy for screenshots/PDF, or use `/scrape` with `format: html` and render locally. | | `POST /v1/extract {url, query}` | `POST /extract {url, instruction}` or `{url, schema}` | `query` → `instruction` (plain English), or give a JSON `schema` for typed records; `content` instead of `url` to extract from your own text. Response: `records[]`. | | `POST /v1/extract/async {url, query, extra_urls[]}` | `POST /extract` per URL (or `/scrape/batch` then `/extract` with `content`) | no schema reuse endpoint yet (`/v1/schemas` has no twin): pass the same `schema` each call. | | `POST /v1/discovery/search {query}` | `GET /search?q=...` | ranked results as JSON (`results[]` with url, title, snippet); `rich=1` adds follow-ups, related searches, entities. | | `POST /v1/scan {url, mode: "scan"}` (URL discovery) | recipes (not yet released) | until then: `POST /scrape {url, parse: {links: true}}` gives the page's links; loop with `/scrape/batch`. | | `POST /v1/site {url, max_depth, max_urls}` + `GET /v1/site/jobs/{id}` (deep crawl) | recipes (not yet released) | until then: collect URLs with `parse.links` and fetch them with `/scrape/batch` or `/scrape/jobs`. | | `POST /v1/enrich/async {urls[], ...}` | `/search` + `/extract` | search for the URLs, extract with one `schema`; no multi-phase job. | | `POST /v1/context` (Context v2, versioned runs) | not offered | keep the legacy for context runs. | | `POST /v1/sessions`, `POST /v1/profiles`, live browsers (CDP) | not offered | the new API runs the browsers for you: use `/scrape` with `proxy: "residential"` and `country` for sites that block datacenters. Sites that need your login: some recipes take a `session` cookie (not yet released). | ## A rewritten function, Python ```python # before (legacy) r = requests.post("https://api.crawl4ai.com/v1/scrape", headers={"X-API-Key": KEY}, json={"url": url}).json() md = r["markdown"] # after r = requests.post("https://api.crawl4ai.com/scrape", headers={"Authorization": f"Bearer {KEY}"}, json={"url": url, "format": "md"}).json() md = r["markdown"] ``` ## The same, Node ```js // before (legacy) const r = await fetch("https://api.crawl4ai.com/v1/extract", { method: "POST", headers: { "X-API-Key": KEY, "content-type": "application/json" }, body: JSON.stringify({ url, query: "product name, price" }) }).then(x => x.json()); // after const r = await fetch("https://api.crawl4ai.com/extract", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "content-type": "application/json" }, body: JSON.stringify({ url, instruction: "the product name and price" }) }).then(x => x.json()); const rows = r.data; ``` ## Checklist for the agent 1. Replace the base URL and the auth header everywhere (grep `X-API-Key`, `/v1/`). 2. Rewrite each call with the table; drop the polling code for `/scrape/async`; stream `/scrape/batch` instead. 3. Mark what has no twin yet (screenshot, context, sessions, scan, site) and tell the user; keep those calls on `api-old.crawl4ai.com` if they must stay. 4. Run the user's tests; show the diff; say what was left on the legacy and why. 5. From an MCP client, the same calls are the tools `scrape`, `search`, `extract`, `batch` (the config: https://crawl4ai.com/docs#mcp).