> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tilion.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Read a protected page

> Fetch rendered text and wait for challenge handling to finish.

`POST /v1/fetch` loads a URL, attempts challenge recovery, and returns rendered text. Use an asynchronous request for protected pages, which can exceed the synchronous 55-second response limit.

Start with a [running session](https://docs.tilion.dev/guides/create-browser.md). Set `TILION_API_KEY`, `TILION_SESSION_ID`, and `TARGET_URL` in your environment.

## Fetch and poll

```python Python theme={"dark"}
import os
import time
import requests

base = "https://back.tilion.dev"
headers = {"Authorization": f"Bearer {os.environ['TILION_API_KEY']}"}
response = requests.post(
    f"{base}/v1/fetch", headers=headers,
    json={
        "session_id": os.environ["TILION_SESSION_ID"],
        "url": os.environ["TARGET_URL"], "async_mode": True,
    }, timeout=70,
)
response.raise_for_status()
started = response.json()
deadline = time.monotonic() + 900
while True:
    if time.monotonic() >= deadline:
        raise TimeoutError("Polling stopped. Check or cancel the job before reusing the session.")
    response = requests.get(
        f"{base}/v1/job/{started['job_id']}", headers=headers, timeout=30
    )
    response.raise_for_status()
    job = response.json()
    if job["status"] != "pending":
        break
    time.sleep(2)
page = job.get("result") or {}
if job["status"] != "done" or not page.get("content_ok"):
    raise RuntimeError(f"Page not delivered: {job}")
print(page["text"])
```

```javascript Node.js theme={"dark"}
const base = "https://back.tilion.dev";
const headers = {
  Authorization: `Bearer ${process.env.TILION_API_KEY}`,
  "Content-Type": "application/json",
};
async function read(response) {
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
const started = await read(await fetch(`${base}/v1/fetch`, {
  method: "POST", headers,
  body: JSON.stringify({
    session_id: process.env.TILION_SESSION_ID,
    url: process.env.TARGET_URL, async_mode: true,
  }), signal: AbortSignal.timeout(70_000),
}));
const deadline = Date.now() + 900_000;
let job;
do {
  if (Date.now() >= deadline) {
    throw new Error("Polling stopped. Check or cancel the job before reusing the session.");
  }
  job = await read(await fetch(`${base}/v1/job/${started.job_id}`, {
    headers, signal: AbortSignal.timeout(30_000),
  }));
  if (job.status === "pending") {
    await new Promise(resolve => setTimeout(resolve, 2_000));
  }
} while (job.status === "pending");
if (job.status !== "done" || !job.result?.content_ok) {
  throw new Error(JSON.stringify(job));
}
console.log(job.result.text);
```

```bash curl theme={"dark"}
# Requires jq. The first response contains the job_id.
curl --fail-with-body https://back.tilion.dev/v1/fetch \
  -H "Authorization: Bearer $TILION_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg sid "$TILION_SESSION_ID" --arg url "$TARGET_URL" \
    '{session_id: $sid, url: $url, async_mode: true}')"

export JOB_ID="paste-the-returned-job_id"
curl --fail-with-body "https://back.tilion.dev/v1/job/$JOB_ID" \
  -H "Authorization: Bearer $TILION_API_KEY"
```

For curl, repeat the GET request while `status` is `pending`. A completed job has `status: "done"` and the fetch response in `result`. Check `result.content_ok` before using its text.

> [!NOTE]
>
> Disconnect the main CDP client and any native Playwright or VNC client before fetching. [CDP observers](https://docs.tilion.dev/guides/multiple-cdp-clients.md) can remain connected; coordinate their commands with the fetch. A session supports one running job at a time.

`premium_required` identifies a restriction preventing premium recovery. `recovery` describes the attempts made. See [Fetch & jobs](https://docs.tilion.dev/reading/fetch.md) for all outcomes and job cancellation. Close the session when processing is complete.

On a [custom-proxy session](https://docs.tilion.dev/guides/custom-proxy.md), recovery keeps your proxy and never switches to a Tilion exit.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.