;
async function getLLMResult(env, prompt: string, schema?: any) {
const model = "@hf/thebloke/deepseek-coder-6.7b-instruct-awq"
const requestBody = {
messages: [{
role: "user",
content: prompt
}],
};
const aiUrl = `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/ai/run/${model}`
const response = await fetch(aiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.API_TOKEN}`,
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
console.log(JSON.stringify(await response.text(), null, 2));
throw new Error(`LLM call failed ${aiUrl} ${response.status}`);
}
// process response
const data = await response.json() as { result: { response: string }};
const text = data.result.response || '';
const value = (text.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || [null, text])[1];
try {
return JSON.parse(value);
} catch(e) {
console.error(`${e} . Response: ${value}`)
}
}
```
You can run this script to test it using Wrangler's `--remote` flag:
```sh
npx wrangler dev --remote
```
With your script now running, you can go to `http://localhost:8787/` and should see something like the following:
```json
{
"title": "IP Addresses in 2024",
"url": "http://example.com/ip-addresses-in-2024",
"date": "11 Jan 2025"
}
```
For more complex websites or prompts, you might need a better model. Check out the latest models in [Workers AI](/workers-ai/models/).
---
# Generate PDFs Using HTML and CSS
URL: https://developers.cloudflare.com/browser-rendering/how-to/pdf-generation/
import { Aside, WranglerConfig } from "~/components";
As seen in the [Getting Started guide](/browser-rendering/workers-binding-api/screenshots/), Browser Rendering can be used to generate screenshots for any given URL. Alongside screenshots, you can also generate full PDF documents for a given webpage, and can also provide the webpage markup and style ourselves.
## Prerequisites
1. Use the `create-cloudflare` CLI to generate a new Hello World Cloudflare Worker script:
```sh
npm create cloudflare@latest -- browser-worker
```
2. Install `@cloudflare/puppeteer`, which allows you to control the Browser Rendering instance:
```sh
npm install @cloudflare/puppeteer --save-dev
```
3. Add your Browser Rendering binding to your new Wrangler configuration:
```toml title="wrangler.toml"
browser = { binding = "BROWSER" }
```
4. Replace the contents of `src/index.ts` (or `src/index.js` for JavaScript projects) with the following skeleton script:
```ts
import puppeteer from "@cloudflare/puppeteer";
const generateDocument = (name: string) => {};
export default {
async fetch(request, env) {
const { searchParams } = new URL(request.url);
let name = searchParams.get("name");
if (!name) {
return new Response("Please provide a name using the ?name= parameter");
}
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
// Step 1: Define HTML and CSS
const document = generateDocument(name);
// Step 2: Send HTML and CSS to our browser
await page.setContent(document);
// Step 3: Generate and return PDF
return new Response();
},
};
```
## 1. Define HTML and CSS
Rather than using Browser Rendering to navigate to a user-provided URL, manually generate a webpage, then provide that webpage to the Browser Rendering instance. This allows you to render any design you want.
:::note
You can generate your HTML or CSS using any method you like. This example uses string interpolation, but the method is also fully compatible with web frameworks capable of rendering HTML on Workers such as React, Remix, and Vue.
:::
For this example, we're going to take in user-provided content (via a '?name=' parameter), and have that name output in the final PDF document.
To start, fill out your `generateDocument` function with the following:
```ts
const generateDocument = (name: string) => {
return `
This is to certify that
${name}
has rendered a PDF using Cloudflare Workers
`;
};
```
This example HTML document should render a beige background imitating a certificate showing that the user-provided name has successfully rendered a PDF using Cloudflare Workers.
:::note
It is usually best to avoid directly interpolating user-provided content into an image or PDF renderer in production applications. To render contents like an invoice, it would be best to validate the data input and fetch the data yourself using tools like [D1](/d1/) or [Workers KV](/kv/).
:::
## 2. Load HTML and CSS Into Browser
Now that you have your fully styled HTML document, you can take the contents and send it to your browser instance. Create an empty page to store this document as follows:
```ts
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
```
The [`page.setContent()`](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.setcontent.md) function can then be used to set the page's HTML contents from a string, so you can pass in your created document directly like so:
```ts
await page.setContent(document);
```
## 3. Generate and Return PDF
With your Browser Rendering instance now rendering your provided HTML and CSS, you can use the [`page.pdf()`](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.pdf.md) command to generate a PDF file and return it to the client.
```ts
let pdf = page.pdf({ printBackground: true });
```
The `page.pdf()` call supports a [number of options](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.pdfoptions.md), including setting the dimensions of the generated PDF to a specific paper size, setting specific margins, and allowing fully-transparent backgrounds. For now, you are only overriding the `printBackground` option to allow your `body` background styles to show up.
Now that you have your PDF data, return it to the client in the `Response` with an `application/pdf` content type:
```ts
return new Response(pdf, {
headers: {
"content-type": "application/pdf",
},
});
```
## Conclusion
The full Worker script now looks as follows:
```ts
import puppeteer from "@cloudflare/puppeteer";
const generateDocument = (name: string) => {
return `
This is to certify that
${name}
has rendered a PDF using Cloudflare Workers
`;
};
export default {
async fetch(request, env) {
const { searchParams } = new URL(request.url);
let name = searchParams.get("name");
if (!name) {
return new Response("Please provide a name using the ?name= parameter");
}
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
// Step 1: Define HTML and CSS
const document = generateDocument(name);
// // Step 2: Send HTML and CSS to our browser
await page.setContent(document);
// // Step 3: Generate and return PDF
const pdf = await page.pdf({ printBackground: true });
return new Response(pdf, {
headers: {
"content-type": "application/pdf",
},
});
},
};
```
You can run this script to test it using Wrangler’s `--remote` flag:
```sh
npx wrangler@latest dev --remote
```
With your script now running, you can pass in a `?name` parameter to the local URL (such as `http://localhost:8787/?name=Harley`) and should see the following:
.
---
Dynamically generating PDF documents solves a number of common use-cases, from invoicing customers to archiving documents to creating dynamic certificates (as seen in the simple example here).
---
# Platform
URL: https://developers.cloudflare.com/browser-rendering/platform/
import { DirectoryListing } from "~/components";
---
# Browser close reasons
URL: https://developers.cloudflare.com/browser-rendering/platform/browser-close-reasons/
A browser session may close for a variety of reasons, occasionally due to connection errors or errors in the headless browser instance. As a best practice, wrap `puppeteer.connect` or `puppeteer.launch` in a [`try/catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) statement.
The reason that a browser closed can be found on the Browser Rendering Dashboard in the [logs tab](https://dash.cloudflare.com/?to=/:account/workers/browser-renderingl/logs). When Cloudflare begins charging for the Browser Rendering API, we will not charge when errors are due to underlying Browser Rendering infrastructure.
| Reasons a session may end |
| ---------------------------------------------------- |
| User opens and closes browser normally. |
| Browser is idle for 60 seconds. |
| Chromium instance crashes. |
| Error connecting with the client, server, or Worker. |
| Browser session is evicted. |
---
# Limits
URL: https://developers.cloudflare.com/browser-rendering/platform/limits/
import { Render, Plan } from "~/components";
## Workers Free
| Feature | Limit |
| -------------------------------------- | --------------- |
| Concurrent browsers per account (Workers Binding API only) | 3 per account |
| New browser instances per minute (Workers Binding API only) | 3 per minute |
| Browser timeout | 60 seconds [^2] |
| Total requests per min (REST API only) | 6 per minute |
## Workers Paid
| Feature | Limit |
| -------------------------------------- | ------------------- |
| Concurrent browsers per account (Workers Binding API only) | 10 per account [^1] |
| New browser instances per minute (Workers Binding API only) | 10 per minute [^1] |
| Browser timeout | 60 seconds [^2][^1] |
| Total requests per min (REST API only) | 60 per minute |
[^1]: Contact our team to request increases to this limit.
[^2]: By default, a browser instance gets killed if it does not get any [devtools](https://chromedevtools.github.io/devtools-protocol/) command for 60 seconds, freeing one instance. Users can optionally increase this by using the `keep_alive` [option](/browser-rendering/platform/puppeteer/#keep-alive). `browser.close()` releases the browser instance.
## Note on concurrency
While the limits above define the maximum number of concurrent browser sessions per account, in practice you may not need to hit these limits. Browser sessions close automatically—by default, after 60 seconds of inactivity or upon task completion—so if each session finishes its work before a new request comes in, the effective concurrency is lower. This means that most workflows do not require very high concurrent browser limits.
## Pricing
Browser Rendering service is currently available at no cost up to the limits specified above **until billing begins**. Pricing to be announced and we will provide advance notice before any billing begins.
---
# Playwright
URL: https://developers.cloudflare.com/browser-rendering/platform/playwright/
import { Render, WranglerConfig, TabItem, Tabs } from "~/components";
[Playwright](https://playwright.dev/) is an open-source package developed by Microsoft that can do browser automation tasks; it's commonly used to write frontend tests, create screenshots, or crawl pages.
The Workers team forked a [version of Playwright](https://github.com/cloudflare/playwright) that was modified to be compatible with [Cloudflare Workers](https://developers.cloudflare.com/workers/) and [Browser Rendering](https://developers.cloudflare.com/browser-rendering/).
Our version is open sourced and can be found in [Cloudflare's fork of Playwright](https://github.com/cloudflare/playwright). The npm package can be installed from [npmjs](https://www.npmjs.com/) as [@cloudflare/playwright](https://www.npmjs.com/package/@cloudflare/playwright):
```bash
npm install @cloudflare/playwright --save-dev
```
## Use Playwright in a Worker
Make sure you have the [browser binding](/browser-rendering/platform/wrangler/#bindings) configured in your `wrangler.toml` file:
```toml
name = "cloudflare-playwright-example"
main = "src/index.ts"
workers_dev = true
compatibility_flags = ["nodejs_compat_v2"]
compatibility_date = "2025-03-05"
upload_source_maps = true
[dev]
port = 9000
[browser]
binding = "MYBROWSER"
```
Install the npm package:
```bash
npm install --save-dev @cloudflare/playwright
```
Let's look at some examples of how to use Playwright:
### Take a screenshot
Using browser automation to take screenshots of web pages is a common use case. This script tells the browser to navigate to https://demo.playwright.dev/todomvc, create some items, take a screenshot of the page, and return the image in the response.
```ts
import { launch, type BrowserWorker } from '@cloudflare/playwright';
interface Env {
MYBROWSER: BrowserWorker;
}
export default {
async fetch(request: Request, env: Env) {
const browser = await launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto('https://demo.playwright.dev/todomvc');
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const img = await page.screenshot();
await browser.close();
return new Response(img, {
headers: {
'Content-Type': 'image/png',
},
});
},
}
```
### Trace
A Playwright trace is a detailed log of your workflow execution that captures information like user clicks and navigation actions, screenshots of the page, and any console messages generated and used for debugging. Developers can take a `trace.zip` file and either open it [locally](https://playwright.dev/docs/trace-viewer#opening-the-trace) or upload it to the [Playwright Trace Viewer](https://trace.playwright.dev/), a GUI tool that helps you explore the data.
Here's an example of a worker generating a trace file:
```ts
import { launch, type BrowserWorker } from "@cloudflare/playwright";
import fs from '@cloudflare/playwright/fs';
interface Env {
MYBROWSER: BrowserWorker;
}
export default {
async fetch(request: Request, env: Env) {
const browser = await launch(env.MYBROWSER);
const page = await browser.newPage();
// Start tracing before navigating to the page
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.goto('https://demo.playwright.dev/todomvc');
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Stop tracing and save the trace to a zip file
await page.context().tracing.stop({ path: 'trace.zip' });
await browser.close();
const file = await fs.promises.readFile('trace.zip');
return new Response(file, {
status: 200,
headers: {
'Content-Type': 'application/zip',
},
});
},
};
```
### Assertions
One of the most common use cases for using Playwright is software testing. Playwright includes test assertion features in its APIs; refer to [Assertions](https://playwright.dev/docs/test-assertions) in the Playwright documentation for details. Here's an example of a Worker doing `expect()` test assertions of the [todomvc](https://demo.playwright.dev/todomvc) demo page:
```ts
import { launch, type BrowserWorker } from '@cloudflare/playwright';
import { expect } from '@cloudflare/playwright/test';
interface Env {
MYBROWSER: BrowserWorker;
}
export default {
async fetch(request: Request, env: Env) {
const browser = await launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto('https://demo.playwright.dev/todomvc');
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
await expect(page.getByTestId('todo-title')).toHaveCount(TODO_ITEMS.length);
await Promise.all(TODO_ITEMS.map(
(value, index) => expect(page.getByTestId('todo-title').nth(index)).toHaveText(value)
));
},
};
```
### Keep Alive
If users omit the `browser.close()` statement, the browser instance will stay open, ready to be connected to again and [re-used](/browser-rendering/workers-binding-api/reuse-sessions/) but it will, by default, close automatically after 1 minute of inactivity. Users can optionally extend this idle time up to 10 minutes, by using the `keep_alive` option, set in milliseconds:
```js
const browser = await playwright.launch(env.MYBROWSER, { keep_alive: 600000 });
```
Using the above, the browser will stay open for up to 10 minutes, even if inactive.
## Session management
In order to facilitate browser session management, we've extended the Playwright API with new methods:
### List open sessions
`playwright.sessions()` lists the current running sessions. It will return an output similar to this:
```json
[
{
"connectionId": "2a2246fa-e234-4dc1-8433-87e6cee80145",
"connectionStartTime": 1711621704607,
"sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",
"startTime": 1711621703708
},
{
"sessionId": "565e05fb-4d2a-402b-869b-5b65b1381db7",
"startTime": 1711621703808
}
]
```
Notice that the session `478f4d7d-e943-40f6-a414-837d3736a1dc` has an active worker connection (`connectionId=2a2246fa-e234-4dc1-8433-87e6cee80145`), while session `565e05fb-4d2a-402b-869b-5b65b1381db7` is free. While a connection is active, no other workers may connect to that session.
### List recent sessions
`playwright.history()` lists recent sessions, both open and closed. It's useful to get a sense of your current usage.
```json
[
{
"closeReason": 2,
"closeReasonText": "BrowserIdle",
"endTime": 1711621769485,
"sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",
"startTime": 1711621703708
},
{
"closeReason": 1,
"closeReasonText": "NormalClosure",
"endTime": 1711123501771,
"sessionId": "2be00a21-9fb6-4bb2-9861-8cd48e40e771",
"startTime": 1711123430918
}
]
```
Session `2be00a21-9fb6-4bb2-9861-8cd48e40e771` was closed explicitly with `browser.close()` by the client, while session `478f4d7d-e943-40f6-a414-837d3736a1dc` was closed due to reaching the maximum idle time (check [limits](/browser-rendering/platform/limits/)).
You should also be able to access this information in the dashboard, albeit with a slight delay.
### Active limits
`playwright.limits()` lists your active limits:
```json
{
"activeSessions": [
{ "id": "478f4d7d-e943-40f6-a414-837d3736a1dc" },
{ "id": "565e05fb-4d2a-402b-869b-5b65b1381db7" }
],
"allowedBrowserAcquisitions": 1,
"maxConcurrentSessions": 2,
"timeUntilNextAllowedBrowserAcquisition": 0
}
```
- `activeSessions` lists the IDs of the current open sessions
- `maxConcurrentSessions` defines how many browsers can be open at the same time
- `allowedBrowserAcquisitions` specifies if a new browser session can be opened according to the rate [limits](/browser-rendering/platform/limits/) in place
- `timeUntilNextAllowedBrowserAcquisition` defines the waiting period before a new browser can be launched.
## Playwright API
The full Playwright API can be found [here](https://playwright.dev/docs/api/class-playwright).
Note that `@cloudflare/playwright` is in beta. The following capabilities are not yet fully supported, but we’re actively working on them:
- [API Testing](https://playwright.dev/docs/api-testing)
- [Playwright Test](https://playwright.dev/docs/test-configuration) except [Assertions](https://playwright.dev/docs/test-assertions)
- [Components](https://playwright.dev/docs/test-components)
- [Firefox](https://playwright.dev/docs/api/class-playwright#playwright-firefox), [Android](https://playwright.dev/docs/api/class-android) and [Electron](https://playwright.dev/docs/api/class-electron), as well as different versions of Chrome
- [Network](https://playwright.dev/docs/next/network#network)
- [Videos](https://playwright.dev/docs/next/videos)
This is **not an exhaustive list** — expect rapid changes as we work toward broader parity with the original feature set. You can also check [latest test results](https://playwright-full-test-report.pages.dev/) for a granular up to date list of the features that are fully supported.
---
# Puppeteer
URL: https://developers.cloudflare.com/browser-rendering/platform/puppeteer/
import { TabItem, Tabs } from "~/components";
[Puppeteer](https://pptr.dev/) is one of the most popular libraries that abstract the lower-level DevTools protocol from developers and provides a high-level API that you can use to easily instrument Chrome/Chromium and automate browsing sessions. Puppeteer is used for tasks like creating screenshots, crawling pages, and testing web applications.
Puppeteer typically connects to a local Chrome or Chromium browser using the DevTools port. Refer to the [Puppeteer API documentation on the `Puppeteer.connect()` method](https://pptr.dev/api/puppeteer.puppeteer.connect) for more information.
The Workers team forked a version of Puppeteer and patched it to connect to the Workers Browser Rendering API instead. After connecting, the developers can then use the full [Puppeteer API](https://github.com/cloudflare/puppeteer/blob/main/docs/api/index.md) as they would on a standard setup.
Our version is open sourced and can be found in [Cloudflare's fork of Puppeteer](https://github.com/cloudflare/puppeteer). The npm can be installed from [npmjs](https://www.npmjs.com/) as [@cloudflare/puppeteer](https://www.npmjs.com/package/@cloudflare/puppeteer):
```bash
npm install @cloudflare/puppeteer --save-dev
```
## Use Puppeteer in a Worker
Once the [browser binding](/browser-rendering/platform/wrangler/#bindings) is configured and the `@cloudflare/puppeteer` library is installed, Puppeteer can be used in a Worker:
```js
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto("https://example.com");
const metrics = await page.metrics();
await browser.close();
return Response.json(metrics);
},
};
```
```ts
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request, env): Promise {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto("https://example.com");
const metrics = await page.metrics();
await browser.close();
return Response.json(metrics);
},
} satisfies ExportedHandler;
```
This script [launches](https://pptr.dev/api/puppeteer.puppeteernode.launch) the `env.MYBROWSER` browser, opens a [new page](https://pptr.dev/api/puppeteer.browser.newpage), [goes to](https://pptr.dev/api/puppeteer.page.goto) [https://example.com/](https://example.com/), gets the page load [metrics](https://pptr.dev/api/puppeteer.page.metrics), [closes](https://pptr.dev/api/puppeteer.browser.close) the browser and prints metrics in JSON.
### Keep Alive
If users omit the `browser.close()` statement, it will stay open, ready to be connected to again and [re-used](/browser-rendering/workers-binding-api/reuse-sessions/) but it will, by default, close automatically after 1 minute of inactivity. Users can optionally extend this idle time up to 10 minutes, by using the `keep_alive` option, set in milliseconds:
```js
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
```
Using the above, the browser will stay open for up to 10 minutes, even if inactive.
## Session management
In order to facilitate browser session management, we've added new methods to `puppeteer`:
### List open sessions
`puppeteer.sessions()` lists the current running sessions. It will return an output similar to this:
```json
[
{
"connectionId": "2a2246fa-e234-4dc1-8433-87e6cee80145",
"connectionStartTime": 1711621704607,
"sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",
"startTime": 1711621703708
},
{
"sessionId": "565e05fb-4d2a-402b-869b-5b65b1381db7",
"startTime": 1711621703808
}
]
```
Notice that the session `478f4d7d-e943-40f6-a414-837d3736a1dc` has an active worker connection (`connectionId=2a2246fa-e234-4dc1-8433-87e6cee80145`), while session `565e05fb-4d2a-402b-869b-5b65b1381db7` is free. While a connection is active, no other workers may connect to that session.
### List recent sessions
`puppeteer.history()` lists recent sessions, both open and closed. It's useful to get a sense of your current usage.
```json
[
{
"closeReason": 2,
"closeReasonText": "BrowserIdle",
"endTime": 1711621769485,
"sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",
"startTime": 1711621703708
},
{
"closeReason": 1,
"closeReasonText": "NormalClosure",
"endTime": 1711123501771,
"sessionId": "2be00a21-9fb6-4bb2-9861-8cd48e40e771",
"startTime": 1711123430918
}
]
```
Session `2be00a21-9fb6-4bb2-9861-8cd48e40e771` was closed explicitly with `browser.close()` by the client, while session `478f4d7d-e943-40f6-a414-837d3736a1dc` was closed due to reaching the maximum idle time (check [limits](/browser-rendering/platform/limits/)).
You should also be able to access this information in the dashboard, albeit with a slight delay.
### Active limits
`puppeteer.limits()` lists your active limits:
```json
{
"activeSessions": [
"478f4d7d-e943-40f6-a414-837d3736a1dc",
"565e05fb-4d2a-402b-869b-5b65b1381db7"
],
"allowedBrowserAcquisitions": 1,
"maxConcurrentSessions": 2,
"timeUntilNextAllowedBrowserAcquisition": 0
}
```
- `activeSessions` lists the IDs of the current open sessions
- `maxConcurrentSessions` defines how many browsers can be open at the same time
- `allowedBrowserAcquisitions` specifies if a new browser session can be opened according to the rate [limits](/browser-rendering/platform/limits/) in place
- `timeUntilNextAllowedBrowserAcquisition` defines the waiting period before a new browser can be launched.
## Puppeteer API
The full Puppeteer API can be found in the [Cloudflare's fork of Puppeteer](https://github.com/cloudflare/puppeteer/blob/main/docs/api/index.md).
---
# Wrangler
URL: https://developers.cloudflare.com/browser-rendering/platform/wrangler/
import { Render, WranglerConfig } from "~/components"
[Wrangler](/workers/wrangler/) is a command-line tool for building with Cloudflare developer products.
Use Wrangler to deploy projects that use the Workers Browser Rendering API.
## Install
To install Wrangler, refer to [Install and Update Wrangler](/workers/wrangler/install-and-update/).
## Bindings
[Bindings](/workers/runtime-apis/bindings/) allow your Workers to interact with resources on the Cloudflare developer platform. A browser binding will provide your Worker with an authenticated endpoint to interact with a dedicated Chromium browser instance.
To deploy a Browser Rendering Worker, you must declare a [browser binding](/workers/runtime-apis/bindings/) in your Worker's Wrangler configuration file.
```toml
# Top-level configuration
name = "browser-rendering"
main = "src/index.ts"
workers_dev = true
compatibility_flags = ["nodejs_compat_v2"]
browser = { binding = "MYBROWSER" }
```
After the binding is declared, access the DevTools endpoint using `env.MYBROWSER` in your Worker code:
```javascript
const browser = await puppeteer.launch(env.MYBROWSER);
```
Run [`npx wrangler dev --remote`](/workers/wrangler/commands/#dev) to test your Worker remotely before deploying to Cloudflare's global network. Local mode support does not exist for Browser Rendering so `--remote` is required. To deploy, run [`npx wrangler deploy`](/workers/wrangler/commands/#deploy).
---
# Fetch HTML
URL: https://developers.cloudflare.com/browser-rendering/rest-api/content-endpoint/
import { Tabs, TabItem } from "~/components";
The `/content` endpoint instructs the browser to navigate to a website and capture the fully rendered HTML of a page, including the `head` section, after JavaScript execution. This is ideal for capturing content from JavaScript-heavy or interactive websites.
## Basic usage
Go to `https://example.com` and return the rendered HTML.
```bash
curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/content' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer ' \
-d '{"url": "https://example.com"}'
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const content = await client.browserRendering.content.create({
account_id: "account_id",
});
console.log(content);
```
## Advanced usage
Navigate to `https://cloudflare.com/` but block images and stylesheets from loading. Undesired requests can be blocked by resource type (`rejectResourceTypes`) or by using a regex pattern (`rejectRequestPattern`). The opposite can also be done, only allow requests that match `allowRequestPattern` or `allowResourceTypes`.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/content' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://cloudflare.com/",
"rejectResourceTypes": ["image"],
"rejectRequestPattern": ["/^.*\\.(css)"]
}'
```
Many more options exist, like setting HTTP headers using `setExtraHTTPHeaders`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](/api/resources/browser_rendering/subresources/content/methods/create/) for all available parameters.
---
# REST API
URL: https://developers.cloudflare.com/browser-rendering/rest-api/
The REST API is a RESTful interface that provides endpoints for common browser actions such as capturing screenshots, extracting HTML content, generating PDFs, and more.
The following are the available options:
import { DirectoryListing } from "~/components";
Use the REST API when you need a fast, simple way to perform common browser tasks such as capturing screenshots, extracting HTML, or generating PDFs without writing complex scripts. If you require more advanced automation, custom workflows, or persistent browser sessions, the [Workers Binding API](/browser-rendering/workers-binding-api/) is the better choice.
## Before you begin
Before you begin, make sure you [create a custom API Token](/fundamentals/api/get-started/create-token/) with the following permissions:
- `Browser Rendering - Edit`
:::note[Note]
Currently, the Cloudflare dashboard displays usage metrics exclusively for the [Workers Binding API method](/browser-rendering/workers-binding-api/). Usage data for the REST API is not yet available in the dashboard. We are actively working on adding REST API usage metrics to the dashboard.
:::
---
# Capture structured data
URL: https://developers.cloudflare.com/browser-rendering/rest-api/json-endpoint/
import { Tabs, TabItem } from "~/components";
The `/json` endpoint extracts structured data from a webpage. You can specify the expected output using either a `prompt` or a `response_format` parameter which accepts a JSON schema. The endpoint returns the extracted data in JSON format.
:::note[Note]
The `/json` endpoint leverages [Workers AI](/workers-ai/) for data extraction. Using this endpoint incurs usage on Workers AI, which you can monitor usage through the Workers AI Dashboard.
:::
## Basic Usage
### With a Prompt and JSON schema
This example captures webpage data by providing both a prompt and a JSON schema. The prompt guides the extraction process, while the JSON schema defines the expected structure of the output.
```bash
curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \
--header 'authorization: Bearer CF_API_TOKEN' \
--header 'content-type: application/json' \
--data '{
"url": "https://developers.cloudflare.com/",
"prompt": "Get me the list of AI products",
"response_format": {
"type": "json_schema",
"json_schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"link": {
"type": "string"
}
},
"required": [
"name"
]
}
}
}
}
}
}'
```
```json output collapse={15-48}
{
"success": true,
"result": {
"products": [
{
"name": "Build a RAG app",
"link": "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/"
},
{
"name": "Workers AI",
"link": "https://developers.cloudflare.com/workers-ai/"
},
{
"name": "Vectorize",
"link": "https://developers.cloudflare.com/vectorize/"
},
{
"name": "AI Gateway",
"link": "https://developers.cloudflare.com/ai-gateway/"
},
{
"name": "AI Playground",
"link": "https://playground.ai.cloudflare.com/"
}
]
}
}
```
### With only a prompt
In this example, only a prompt is provided. The endpoint will use the prompt to extract the data, but the response will not be structured according to a JSON schema.
This is useful for simple extractions where you don't need a specific format.
```bash
curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \
--header 'authorization: Bearer CF_API_TOKEN' \
--header 'content-type: application/json' \
--data '{
"url": "https://developers.cloudflare.com/",
"prompt": "get me the list of AI products"
}'
```
```json output
"success": true,
"result": {
"AI Products": [
"Build a RAG app",
"Workers AI",
"Vectorize",
"AI Gateway",
"AI Playground"
]
}
}
```
### With only a JSON schema (no prompt)
In this case, you supply a JSON schema via the `response_format` parameter. The schema defines the structure of the extracted data.
```bash
curl --request POST 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/browser-rendering/json' \
--header 'authorization: Bearer CF_API_TOKEN' \
--header 'content-type: application/json' \
--data '"response_format": {
"type": "json_schema",
"json_schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"link": {
"type": "string"
}
},
"required": [
"name"
]
}
}
}
}
}'
```
```json output collapse={13-68}
{
"success": true,
"result": {
"products": [
{
"name": "Workers",
"link": "https://developers.cloudflare.com/workers/"
},
{
"name": "Pages",
"link": "https://developers.cloudflare.com/pages/"
},
{
"name": "R2",
"link": "https://developers.cloudflare.com/r2/"
},
{
"name": "Images",
"link": "https://developers.cloudflare.com/images/"
},
{
"name": "Stream",
"link": "https://developers.cloudflare.com/stream/"
},
{
"name": "Build a RAG app",
"link": "https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/"
},
{
"name": "Workers AI",
"link": "https://developers.cloudflare.com/workers-ai/"
},
{
"name": "Vectorize",
"link": "https://developers.cloudflare.com/vectorize/"
},
{
"name": "AI Gateway",
"link": "https://developers.cloudflare.com/ai-gateway/"
},
{
"name": "AI Playground",
"link": "https://playground.ai.cloudflare.com/"
},
{
"name": "Access",
"link": "https://developers.cloudflare.com/cloudflare-one/policies/access/"
},
{
"name": "Tunnel",
"link": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"
},
{
"name": "Gateway",
"link": "https://developers.cloudflare.com/cloudflare-one/policies/gateway/"
},
{
"name": "Browser Isolation",
"link": "https://developers.cloudflare.com/cloudflare-one/policies/browser-isolation/"
},
{
"name": "Replace your VPN",
"link": "https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/"
}
]
}
}
```
Below is an example using the TypeScript SDK:
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const json = await client.browserRendering.json.create({
account_id: "account_id",
});
console.log(json);
```
---
# Retrieve links from a webpage
URL: https://developers.cloudflare.com/browser-rendering/rest-api/links-endpoint/
import { Tabs, TabItem } from "~/components";
The `/links` endpoint retrieves all links from a webpage. It can be used to extract all links from a page, including those that are hidden.
## Basic usage
This example grabs all links from the Cloudflare Developers homepage.
The response will be a JSON array containing the links found on the page.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/links' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://developers.cloudflare.com/"
}'
```
```json output collapse={12-80}
{
"success": true,
"result": [
"https://developers.cloudflare.com/",
"https://developers.cloudflare.com/products/",
"https://developers.cloudflare.com/api/",
"https://developers.cloudflare.com/fundamentals/api/reference/sdks/",
"https://dash.cloudflare.com/",
"https://developers.cloudflare.com/fundamentals/subscriptions-and-billing/",
"https://developers.cloudflare.com/api/",
"https://developers.cloudflare.com/changelog/",
"https://developers.cloudflare.com/glossary/",
"https://developers.cloudflare.com/reference-architecture/",
"https://developers.cloudflare.com/web-analytics/",
"https://developers.cloudflare.com/support/troubleshooting/http-status-codes/",
"https://developers.cloudflare.com/registrar/",
"https://developers.cloudflare.com/1.1.1.1/setup/",
"https://developers.cloudflare.com/learning-paths/get-started/concepts/",
"https://developers.cloudflare.com/workers/",
"https://developers.cloudflare.com/pages/",
"https://developers.cloudflare.com/r2/",
"https://developers.cloudflare.com/images/",
"https://developers.cloudflare.com/stream/",
"https://developers.cloudflare.com/products/?product-group=Developer+platform",
"https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",
"https://developers.cloudflare.com/workers-ai/",
"https://developers.cloudflare.com/vectorize/",
"https://developers.cloudflare.com/ai-gateway/",
"https://playground.ai.cloudflare.com/",
"https://developers.cloudflare.com/products/?product-group=AI",
"https://developers.cloudflare.com/cloudflare-one/policies/access/",
"https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
"https://developers.cloudflare.com/cloudflare-one/policies/gateway/",
"https://developers.cloudflare.com/cloudflare-one/policies/browser-isolation/",
"https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/",
"https://developers.cloudflare.com/products/?product-group=Cloudflare+One",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAmAIyiAzMIAsATlmi5ALhYs2wDnC40+AkeKlyFcgLAAoAMLoqEAKY3sAESgBnGOhdRo1pSXV4CYhIqOGBbBgAiKBpbAA8AOgArFwjSVCgwe1DwqJiE5IjzKxt7CGwAFToYW184GBgwPgIoa2REuAA3OBdeBFgIAGpgdFxwW3NzOPckElxbVDhwCBIAbzMSEm66Kl4-WwheAAsACgRbAEcQWxcIAEpV9Y2SXmsbkkOIYDASBhIAAwAPABCRwAeQs5QAmgAFACi70+YAAfI8NgCKLg6Cink8AYdREiABK2MBgdAkADqmDAuAByHx2JxJABMCR5UOrhIwEQAGsQDASAB3bokADm9lsCAItlw5DomxIFjJIFwqDAiFslMwPMl8TprNRzOQGKxfyIZkNZwgIAQVGCtkFJAAStd3FQXLZjh8vgAaB5M962OBzBAuXxrAMbCIvEoOCBVWwRXwROyxFDesBEI6ID0QBgAVXKADFsAAOCI+w0bAC+lZx1du5prlerRHMqmY6k02h4-CEYkkMnkilkRWsdgczjcHi8LSovn8mlIITCkTChE0qT8GSyq4iZDJZEKlnHpQqCdq9UavGarWS1gmZhWEW50QA+sNRpkk7k5vkUtW7Ydl2gQ9ro-YGEOxiyMwQA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwB2AMwAWAKyCAjMICc8meIBcLFm2Ac4XGnwEiJ0uYuUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWCZ5IJLj2qHDgECQA3hYkJL10VLwB9hC8ABYAFAj2AI4g9m4QAJTrm1skvLZ388EkDE8vL8f2MBgdD+KIAd0wYFwUQANM8tgBfIgWeEkC4QEAIKgkABKt08VDc9hSblsp2092RiLhSMs6mYmm0uh4-CEYiksgUSnEJVsDicrg8Xh8bSo-kC2lIYQi0QihG06QCWRyMqiZGBZGK1j55SqNTq20azV4rXaqVsUwsayiwDgsQA+qNxtkoip8gtCmkEXT6Yzgsz9GyjJzTOJmEA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBWABwBGAOyjRANgDMAFgCcygFwsWbYBzhcafASInS5S1QFgAUAGF0VCAFMH2ACJQAzjHQeo0e2ok2ngExCRUcMCODABEUDSOAB4AdABWHjGkqFBgzpHRcQkp6THWdg7OENgAKnQwjoFwMDBgfARQ9sipcABucB68CLAQANTA6LjgjtbWSd5IJLiOqHDgECQA3lYkJP10VLxBjhC8ABYAFAiOAI4gjh4QAJSb2zskyABUH69vHyQASo4WnBeI4SAADK7jJzgkgAdz8pxIEFOYNOPnWdEo8M8SIg6BIHmcuBIV1u9wgHmR6B+Ow+yFpvHsD1JjmhYIYJBipwgEBgHjUyGQSUiLUcySZwEyVlpVwgIAQVF2cLgfiOJwuUPQTgANKzyQ9HkRXgBfHVWE1EayaZjaXT6Hj8IRiKQyBQqZRlexOFzuLw+PwdKiBYK6UgRKKxKKEXSZII5PKRmJkMDoMilWzeyo1OoNXbNVq8dqddL2GZWDYxYCqqgAfXGk1yMTUhSWxQyJutNrtoQdhmdJjd5mUzCAA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBmACyiAnBMFSAbIICMALhYs2wDnC40+AkeKkyJ8hQFgAUAGF0VCAFNb2ACJQAzjHSuo0G0pLq8AmISKjhgOwYAIigaOwAPADoAK1dI0lQoMAcwiOjYxJTIi2tbBwhsABU6GDs-OBgYMD4CKBtkJLgANzhXXgRYCABqYHRccDsLC3iPJBJcO1Q4cAgSAG9zEhIeuipefzsIXgALAAoEOwBHEDtXCABKNY3Nkl4bW7mb6FCfKgBVACUADIkBgkSJHCAQGCuJTIZDxMKNOwJV7ANJPTavKjvW4EECuazzEEkYSKIgYkjnCAgBBUEj-G4ebHI848c68CAnea3GItGwAwEAGhIuOpBNGdju5M2AF9BeYZUQLKpmOpNNoePwhGJJNI5IpijZ7I4XO5PN5WlQ-AFNKRQuEouFCJo0v5MtkHZEyGB0GQilYjWVKtValsGk1eHyqO1XDZJuZVpFgHAYgB9EZjLKRJR5eYFVIy5UqtVBDW6bUGPXGRTMIA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAOAJwBmAIyiATKMkB2AKwyAXCxZtgHOFxp8BIidLmKVAWABQAYXRUIAU3vYAIlADOMdO6jQ7qki08AmISKjhgBwYAIigaBwAPADoAK3do0lQoMCcIqNj45LToq1t7JwhsABU6GAcAuBgYMD4CKDtkFLgANzh3XgRYCABqYHRccAcrK0SvJBJcB1Q4cAgSAG9LEhI+uipeQIcIXgALAAoEBwBHEAd3CABKDa3tnfc9g9RqXj8qEgBZI4ncYAOXQEAAgmAwOgAO4OXAXa63e5PTavV6XCAgBB-KgOWEkABKdy8VHcDjOAANARBgbgSAASdaXG53CBJSJ08YAXzC4J20LhCKSVIANM8MRj7gQQO4AgAWQRKMUvKUkE4OOCLBDyyXq15QmGwgLRADiAFEqtFVQaSDzbVKeQ8iGr7W7kMgSAB5KhgOgkS1VEislEQdwkWGYADWkd8JxIdI8JBgCHQCToSTdUFQJCRbPunKB4xIAEIGAwSOardEnlicX9afSwZChfDEaH2S63fXcYdjucqScIBAYPLPYkIs0HEleOhgFTu9sHZYeUQrBpmFodHoePwhGIpLJ5MoZKU7I5nG5PN5fO0qAEgjpSOFIjEudqQhlAtlcm-omQMJkCUNgXhU1S1PUOxNC0vBtB0aR2NMljrNEwBwHEAD6YwTDk0SqAUixFOkPIbpu24hLuBgHsYx5mDIzBAA",
"https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/",
"https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/",
"https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/",
"https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/",
"https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",
"https://discord.cloudflare.com/",
"https://x.com/CloudflareDev",
"https://community.cloudflare.com/",
"https://github.com/cloudflare",
"https://developers.cloudflare.com/sponsorships/",
"https://developers.cloudflare.com/style-guide/",
"https://blog.cloudflare.com/",
"https://developers.cloudflare.com/fundamentals/",
"https://support.cloudflare.com/",
"https://www.cloudflarestatus.com/",
"https://www.cloudflare.com/trust-hub/compliance-resources/",
"https://www.cloudflare.com/trust-hub/gdpr/",
"https://www.cloudflare.com/",
"https://www.cloudflare.com/people/",
"https://www.cloudflare.com/careers/",
"https://radar.cloudflare.com/",
"https://speed.cloudflare.com/",
"https://isbgpsafeyet.com/",
"https://rpki.cloudflare.com/",
"https://ct.cloudflare.com/",
"https://x.com/cloudflare",
"http://discord.cloudflare.com/",
"https://www.youtube.com/cloudflare",
"https://github.com/cloudflare/cloudflare-docs",
"https://www.cloudflare.com/privacypolicy/",
"https://www.cloudflare.com/website-terms/",
"https://www.cloudflare.com/disclosure/",
"https://www.cloudflare.com/trademark/"
]
}
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const links = await client.browserRendering.links.create({
account_id: "account_id",
});
console.log(links);
```
## Advanced usage
In this example we can pass a `visibleLinksOnly` parameter to only return links that are visible on the page.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/links' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://developers.cloudflare.com/",
"visibleLinksOnly": true
}'
```
```json output collapse={12-80}
{
"success": true,
"result": [
"https://developers.cloudflare.com/",
"https://developers.cloudflare.com/products/",
"https://developers.cloudflare.com/api/",
"https://developers.cloudflare.com/fundamentals/api/reference/sdks/",
"https://dash.cloudflare.com/",
"https://developers.cloudflare.com/fundamentals/subscriptions-and-billing/",
"https://developers.cloudflare.com/api/",
"https://developers.cloudflare.com/changelog/",
"https://developers.cloudflare.com/glossary/",
"https://developers.cloudflare.com/reference-architecture/",
"https://developers.cloudflare.com/web-analytics/",
"https://developers.cloudflare.com/support/troubleshooting/http-status-codes/",
"https://developers.cloudflare.com/registrar/",
"https://developers.cloudflare.com/1.1.1.1/setup/",
"https://developers.cloudflare.com/learning-paths/get-started/concepts/",
"https://developers.cloudflare.com/workers/",
"https://developers.cloudflare.com/pages/",
"https://developers.cloudflare.com/r2/",
"https://developers.cloudflare.com/images/",
"https://developers.cloudflare.com/stream/",
"https://developers.cloudflare.com/products/?product-group=Developer+platform",
"https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",
"https://developers.cloudflare.com/workers-ai/",
"https://developers.cloudflare.com/vectorize/",
"https://developers.cloudflare.com/ai-gateway/",
"https://playground.ai.cloudflare.com/",
"https://developers.cloudflare.com/products/?product-group=AI",
"https://developers.cloudflare.com/cloudflare-one/policies/access/",
"https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/",
"https://developers.cloudflare.com/cloudflare-one/policies/gateway/",
"https://developers.cloudflare.com/cloudflare-one/policies/browser-isolation/",
"https://developers.cloudflare.com/learning-paths/replace-vpn/concepts/",
"https://developers.cloudflare.com/products/?product-group=Cloudflare+One",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAmAIyiAzMIAsATlmi5ALhYs2wDnC40+AkeKlyFcgLAAoAMLoqEAKY3sAESgBnGOhdRo1pSXV4CYhIqOGBbBgAiKBpbAA8AOgArFwjSVCgwe1DwqJiE5IjzKxt7CGwAFToYW184GBgwPgIoa2REuAA3OBdeBFgIAGpgdFxwW3NzOPckElxbVDhwCBIAbzMSEm66Kl4-WwheAAsACgRbAEcQWxcIAEpV9Y2SXmsbkkOIYDASBhIAAwAPABCRwAeQs5QAmgAFACi70+YAAfI8NgCKLg6Cink8AYdREiABK2MBgdAkADqmDAuAByHx2JxJABMCR5UOrhIwEQAGsQDASAB3bokADm9lsCAItlw5DomxIFjJIFwqDAiFslMwPMl8TprNRzOQGKxfyIZkNZwgIAQVGCtkFJAAStd3FQXLZjh8vgAaB5M962OBzBAuXxrAMbCIvEoOCBVWwRXwROyxFDesBEI6ID0QBgAVXKADFsAAOCI+w0bAC+lZx1du5prlerRHMqmY6k02h4-CEYkkMnkilkRWsdgczjcHi8LSovn8mlIITCkTChE0qT8GSyq4iZDJZEKlnHpQqCdq9UavGarWS1gmZhWEW50QA+sNRpkk7k5vkUtW7Ydl2gQ9ro-YGEOxiyMwQA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwB2AMwAWAKyCAjMICc8meIBcLFm2Ac4XGnwEiJ0uYuUBYAFABhdFQgBTO9gAiUAM4x0bqNFsqSmngExCRUcMD2DABEUDT2AB4AdABWblGkqFBgjuGRMXFJqVGWNnaOENgAKnQw9v5wMDBgfARQtsjJcABucG68CLAQANTA6Ljg9paWCZ5IJLj2qHDgECQA3hYkJL10VLwB9hC8ABYAFAj2AI4g9m4QAJTrm1skvLZ388EkDE8vL8f2MBgdD+KIAd0wYFwUQANM8tgBfIgWeEkC4QEAIKgkABKt08VDc9hSblsp2092RiLhSMs6mYmm0uh4-CEYiksgUSnEJVsDicrg8Xh8bSo-kC2lIYQi0QihG06QCWRyMqiZGBZGK1j55SqNTq20azV4rXaqVsUwsayiwDgsQA+qNxtkoip8gtCmkEXT6Yzgsz9GyjJzTOJmEA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBWABwBGAOyjRANgDMAFgCcygFwsWbYBzhcafASInS5S1QFgAUAGF0VCAFMH2ACJQAzjHQeo0e2ok2ngExCRUcMCODABEUDSOAB4AdABWHjGkqFBgzpHRcQkp6THWdg7OENgAKnQwjoFwMDBgfARQ9sipcABucB68CLAQANTA6LjgjtbWSd5IJLiOqHDgECQA3lYkJP10VLxBjhC8ABYAFAiOAI4gjh4QAJSb2zskyABUH69vHyQASo4WnBeI4SAADK7jJzgkgAdz8pxIEFOYNOPnWdEo8M8SIg6BIHmcuBIV1u9wgHmR6B+Ow+yFpvHsD1JjmhYIYJBipwgEBgHjUyGQSUiLUcySZwEyVlpVwgIAQVF2cLgfiOJwuUPQTgANKzyQ9HkRXgBfHVWE1EayaZjaXT6Hj8IRiKQyBQqZRlexOFzuLw+PwdKiBYK6UgRKKxKKEXSZII5PKRmJkMDoMilWzeyo1OoNXbNVq8dqddL2GZWDYxYCqqgAfXGk1yMTUhSWxQyJutNrtoQdhmdJjd5mUzCAA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwBmACyiAnBMFSAbIICMALhYs2wDnC40+AkeKkyJ8hQFgAUAGF0VCAFNb2ACJQAzjHSuo0G0pLq8AmISKjhgOwYAIigaOwAPADoAK1dI0lQoMAcwiOjYxJTIi2tbBwhsABU6GDs-OBgYMD4CKBtkJLgANzhXXgRYCABqYHRccDsLC3iPJBJcO1Q4cAgSAG9zEhIeuipefzsIXgALAAoEOwBHEDtXCABKNY3Nkl4bW7mb6FCfKgBVACUADIkBgkSJHCAQGCuJTIZDxMKNOwJV7ANJPTavKjvW4EECuazzEEkYSKIgYkjnCAgBBUEj-G4ebHI848c68CAnea3GItGwAwEAGhIuOpBNGdju5M2AF9BeYZUQLKpmOpNNoePwhGJJNI5IpijZ7I4XO5PN5WlQ-AFNKRQuEouFCJo0v5MtkHZEyGB0GQilYjWVKtValsGk1eHyqO1XDZJuZVpFgHAYgB9EZjLKRJR5eYFVIy5UqtVBDW6bUGPXGRTMIA",
"https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbPYZb6HbW5QDGU2AAwAOAJwBmAIyiATKMkB2AKwyAXCxZtgHOFxp8BIidLmKVAWABQAYXRUIAU3vYAIlADOMdO6jQ7qki08AmISKjhgBwYAIigaBwAPADoAK3do0lQoMCcIqNj45LToq1t7JwhsABU6GAcAuBgYMD4CKDtkFLgANzh3XgRYCABqYHRccAcrK0SvJBJcB1Q4cAgSAG9LEhI+uipeQIcIXgALAAoEBwBHEAd3CABKDa3tnfc9g9RqXj8qEgBZI4ncYAOXQEAAgmAwOgAO4OXAXa63e5PTavV6XCAgBB-KgOWEkABKdy8VHcDjOAANARBgbgSAASdaXG53CBJSJ08YAXzC4J20LhCKSVIANM8MRj7gQQO4AgAWQRKMUvKUkE4OOCLBDyyXq15QmGwgLRADiAFEqtFVQaSDzbVKeQ8iGr7W7kMgSAB5KhgOgkS1VEislEQdwkWGYADWkd8JxIdI8JBgCHQCToSTdUFQJCRbPunKB4xIAEIGAwSOardEnlicX9afSwZChfDEaH2S63fXcYdjucqScIBAYPLPYkIs0HEleOhgFTu9sHZYeUQrBpmFodHoePwhGIpLJ5MoZKU7I5nG5PN5fO0qAEgjpSOFIjEudqQhlAtlcm-omQMJkCUNgXhU1S1PUOxNC0vBtB0aR2NMljrNEwBwHEAD6YwTDk0SqAUixFOkPIbpu24hLuBgHsYx5mDIzBAA",
"https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/",
"https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/",
"https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/",
"https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/",
"https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",
"https://discord.cloudflare.com/",
"https://x.com/CloudflareDev",
"https://community.cloudflare.com/",
"https://github.com/cloudflare",
"https://developers.cloudflare.com/sponsorships/",
"https://developers.cloudflare.com/style-guide/",
"https://blog.cloudflare.com/",
"https://developers.cloudflare.com/fundamentals/",
"https://support.cloudflare.com/",
"https://www.cloudflarestatus.com/",
"https://www.cloudflare.com/trust-hub/compliance-resources/",
"https://www.cloudflare.com/trust-hub/gdpr/",
"https://www.cloudflare.com/",
"https://www.cloudflare.com/people/",
"https://www.cloudflare.com/careers/",
"https://radar.cloudflare.com/",
"https://speed.cloudflare.com/",
"https://isbgpsafeyet.com/",
"https://rpki.cloudflare.com/",
"https://ct.cloudflare.com/",
"https://x.com/cloudflare",
"http://discord.cloudflare.com/",
"https://www.youtube.com/cloudflare",
"https://github.com/cloudflare/cloudflare-docs",
"https://www.cloudflare.com/privacypolicy/",
"https://www.cloudflare.com/website-terms/",
"https://www.cloudflare.com/disclosure/",
"https://www.cloudflare.com/trademark/"
]
}
```
---
# Extract Markdown from a webpage
URL: https://developers.cloudflare.com/browser-rendering/rest-api/markdown-endpoint/
import { Tabs, TabItem } from "~/components";
The `/markdown` endpoint retrieves a webpage's content and converts it into Markdown format. You can specify a URL and optional parameters to refine the extraction process.
## Basic usage
### Using a URL
This example fetches the Markdown representation of a webpage.
```bash
curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/markdown' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer ' \
-d '{
"url": "https://example.com"
}'
```
```json output
"success": true,
"result": "# Example Domain\n\nThis domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)"
}
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const markdown = await client.browserRendering.markdown.create({
account_id: "account_id",
});
console.log(markdown);
```
### Use raw HTML
Instead of fetching the content by specifying the URL, you can provide raw HTML content directly.
```bash
curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/markdown' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer ' \
-d '{
"html": "Hello World
"
}'
```
```json output
{
"success": true,
"result": "Hello World"
}
```
## Advanced usage
You can refine the Markdown extraction by using the `rejectRequestPattern` parameter. In this example, requests matching the given regex pattern (such as CSS files) are excluded.
```bash
curl -X 'POST' 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/markdown' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer ' \
-d '{
"url": "https://example.com",
"rejectRequestPattern": ["/^.*\\.(css)/"]
}'
```
```json output
{
"success": true,
"result": "# Example Domain\n\nThis domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)"
}
```
## Potential use-cases
1. **Content extraction:** Convert a blog post or article into Markdown format for storage or further processing.
2. **Static site generation:** Retrieve structured Markdown content for use in static site generators like Jekyll or Hugo.
3. **Automated summarization:** Extract key content from web pages while ignoring CSS, scripts, or unnecessary elements.
---
# Render PDF
URL: https://developers.cloudflare.com/browser-rendering/rest-api/pdf-endpoint/
import { Tabs, TabItem } from "~/components";
The `/pdf` endpoint instructs the browser to render the webpage as a PDF document.
## Basic usage
Navigate to `https://example.com/` and inject custom CSS and an external stylesheet. Then return the rendered page as a PDF.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/pdf' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/",
"addStyleTag": [
{ "content": "body { font-family: Arial; }" },
{ "url": "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" }
]
}' \
--output "output.pdf"
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const pdf = await client.browserRendering.pdf.create({
account_id: "account_id",
});
console.log(pdf);
const content = await pdf.blob();
console.log(content);
```
## Advanced usage
Navigate to `https://example.com`, first setting an additional HTTP request header and configuring the page size (`viewport`). Then, wait until there are no more than 2 network connections for at least 500 ms, or until the maximum timeout of 4500 ms is reached, before considering the page loaded and returning the rendered PDF document.
The `goToOptions` parameter exposes most of [Puppeteer'd API](https://pptr.dev/api/puppeteer.gotooptions).
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/pdf' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/",
"setExtraHTTPHeaders": {
"X-Custom-Header": "value"
},
"viewport": {
"width": 1200,
"height": 800
},
"gotoOptions": {
"waitUntil": "networkidle2",
"timeout": 45000
}
}' \
--output "advanced-output.pdf"
```
## Blocking images and styles when generating a PDF
The options `rejectResourceTypes` and `rejectRequestPattern` can be used to block requests. The opposite can also be done, _only_ allow certain requests using `allowResourceTypes` and `allowRequestPattern`.
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts//browser-rendering/pdf \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://cloudflare.com/",
"rejectResourceTypes": ["image"],
"rejectRequestPattern": ["/^.*\\.(css)"]
}' \
--output "cloudflare.pdf"
```
## Generate PDF from custom HTML
If you have HTML you'd like to generate a PDF from, the `html` option can be used. The option `addStyleTag` can be used to add custom styles.
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts//browser-rendering/pdf \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"html": "Advanced Snapshot",
"addStyleTag": [
{ "content": "body { font-family: Arial; }" },
{ "url": "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" }
]
}' \
--output "invoice.pdf"
```
Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](/api/resources/browser_rendering/subresources/pdf/methods/create/) for all available parameters.
---
# Scrape HTML elements
URL: https://developers.cloudflare.com/browser-rendering/rest-api/scrape-endpoint/
import { Tabs, TabItem } from "~/components";
The `/scrape` endpoint extracts structured data from specific elements on a webpage, returning details such as element dimensions and inner HTML.
## Basic usage
Go to `https://example.com` and extract metadata from all `h1` and `a` elements in the DOM.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/scrape' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/",
"elements": [{
"selector": "h1"
},
{
"selector": "a"
}]
}'
```
```json output
{
"success": true,
"result": [
{
"results": [
{
"attributes": [],
"height": 39,
"html": "Example Domain",
"left": 100,
"text": "Example Domain",
"top": 133.4375,
"width": 600
}
],
"selector": "h1"
},
{
"results": [
{
"attributes": [
{ "name": "href", "value": "https://www.iana.org/domains/example" }
],
"height": 20,
"html": "More information...",
"left": 100,
"text": "More information...",
"top": 249.875,
"width": 142
}
],
"selector": "a"
}
]
}
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const scrapes = await client.browserRendering.scrape.create({
account_id: "account_id",
elements: [{ selector: "selector" }],
});
console.log(scrapes);
```
Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](/api/resources/browser_rendering/subresources/scrape/methods/create/) for all available parameters.
### Response fields
- `results` _(array of objects)_ - Contains extracted data for each selector.
- `selector` _(string)_ - The CSS selector used.
- `results` _(array of objects)_ - List of extracted elements matching the selector.
- `text` _(string)_ - Inner text of the element.
- `html` _(string)_ - Inner HTML of the element.
- `attributes` _(array of objects)_ - List of extracted attributes such as `href` for links.
- `height`, `width`, `top`, `left` _(number)_ - Position and dimensions of the element.
---
# Capture screenshot
URL: https://developers.cloudflare.com/browser-rendering/rest-api/screenshot-endpoint/
import { Tabs, TabItem } from "~/components";
The `/screenshot` endpoint renders the webpage by processing its HTML and JavaScript, then captures a screenshot of the fully rendered page.
## Basic usage
Sets the HTML content of the page to `Hello World!` and then takes a screenshot. The option `omitBackground` hides the default white background and allows capturing screenshots with transparency.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/screenshot' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"html": "Hello World!",
"screenshotOptions": {
"omitBackground": true
}
}' \
--output "screenshot.png"
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const screenshot = await client.browserRendering.screenshot.create({
account_id: "account_id",
});
console.log(screenshot.status);
```
For more options to control the final screenshot, like `clip`, `captureBeyondViewport`, `fullPage` and others, check the endpoint [reference](/api/resources/browser_rendering/subresources/screenshot/methods/create/).
## Advanced usage
Navigate to `https://cloudflare.com/`, changing the page size (`viewport`) and waiting until there are no active network connections (`waitUntil`) or up to a maximum of `4500ms` (`timeout`). Then take a `fullPage` screenshot.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/screenshot' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://cnn.com/",
"screenshotOptions": {
"fullPage": true
},
"viewport": {
"width": 1280,
"height": 720
},
"gotoOptions": {
"waitUntil": "networkidle0",
"timeout": 45000
}
}' \
--output "advanced-screenshot.png"
```
## Customize CSS and embed custom JavaScript
Instruct the browser to go to `https://example.com`, embed custom JavaScript (`addScriptTag`) and add extra styles (`addStyleTag`), both inline (`addStyleTag.content`) and by loading an external stylesheet (`addStyleTag.url`).
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/screenshot' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/",
"addScriptTag": [
{ "content": "document.querySelector(`h1`).innerText = `Hello World!!!`" }
],
"addStyleTag": [
{
"content": "div { background: linear-gradient(45deg, #2980b9 , #82e0aa ); }"
},
{
"url": "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
}
]
}' \
--output "screenshot.png"
```
Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](/api/resources/browser_rendering/subresources/screenshot/methods/create/) for all available parameters.
---
# Take a webpage snapshot
URL: https://developers.cloudflare.com/browser-rendering/rest-api/snapshot/
import { Tabs, TabItem } from "~/components";
The `/snapshot` endpoint captures both the HTML content and a screenshot of the webpage in one request. It returns the HTML as a text string and the screenshot as a Base64-encoded image.
## Basic usage
1. Go to `https://example.com/`.
2. Inject custom JavaScript.
3. Capture the rendered HTML.
4. Take a screenshot.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/snapshot' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/",
"addScriptTag": [
{ "content": "document.body.innerHTML = \"Snapshot Page\";" }
]
}'
```
```json output
{
"success": true,
"result": {
"screenshot": "Base64EncodedScreenshotString",
"content": "..."
}
}
```
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"], // This is the default and can be omitted
apiKey: process.env["CLOUDFLARE_API_KEY"], // This is the default and can be omitted
});
const snapshot = await client.browserRendering.snapshot.create({
account_id: "account_id",
});
console.log(snapshot.content);
```
## Advanced usage
The `html` property in the JSON payload, it sets the html to `Advanced Snapshot` then does the following steps:
1. Disable JavaScript.
2. Sets the screenshot to `fullPage`.
3. Changes the page size `(viewport)`.
4. Waits up to `30000ms` or until the `DOMContentLoaded` event fires.
5. Returns the rendered HTML content and a base-64 encoded screenshot of the page.
```bash
curl -X POST 'https://api.cloudflare.com/client/v4/accounts//browser-rendering/snapshot' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/json' \
-d '{
"html": "Advanced Snapshot",
"setJavaScriptEnabled": false,
"screenshotOptions": {
"fullPage": true
},
"viewport": {
"width": 1200,
"height": 800
},
"gotoOptions": {
"waitUntil": "domcontentloaded",
"timeout": 30000
}
}'
```
```json output
{
"success": true,
"result": {
"screenshot": "AdvancedBase64Screenshot",
"content": "Advanced Snapshot"
}
}
```
Many more options exist, like setting HTTP credentials using `authenticate`, setting `cookies`, and using `gotoOptions` to control page load behaviour - check the endpoint [reference](/api/resources/browser_rendering/subresources/snapshot/) for all available parameters.
---
# Workers Binding API
URL: https://developers.cloudflare.com/browser-rendering/workers-binding-api/
import { DirectoryListing } from "~/components";
The Workers Binding API allows you to execute advanced browser rendering scripts within Cloudflare Workers. It provides developers the flexibility to automate and control complex workflows and browser interactions. The following options are available for browser rendering tasks:
Use the Workers Binding API when you need advanced browser automation, custom workflows, or complex interactions beyond basic rendering. For quick, one-off tasks like capturing screenshots or extracting HTML, the [REST API](/browser-rendering/rest-api/) is the simpler choice.
---
# Deploy a Browser Rendering Worker with Durable Objects
URL: https://developers.cloudflare.com/browser-rendering/workers-binding-api/browser-rendering-with-do/
import { Render, PackageManagers, WranglerConfig } from "~/components";
By following this guide, you will create a Worker that uses the Browser Rendering API along with [Durable Objects](/durable-objects/) to take screenshots from web pages and store them in [R2](/r2/).
Using Durable Objects to persist browser sessions improves performance by eliminating the time that it takes to spin up a new browser session. Since Durable Objects re-uses sessions, it reduces the number of concurrent sessions needed.
## 1. Create a Worker project
[Cloudflare Workers](/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.
Create a new Worker project named `browser-worker` by running:
## 2. Install Puppeteer
In your `browser-worker` directory, install Cloudflare’s [fork of Puppeteer](/browser-rendering/platform/puppeteer/):
```sh
npm install @cloudflare/puppeteer --save-dev
```
## 3. Create a R2 bucket
Create two R2 buckets, one for production, and one for development.
Note that bucket names must be lowercase and can only contain dashes.
```sh
wrangler r2 bucket create screenshots
wrangler r2 bucket create screenshots-test
```
To check that your buckets were created, run:
```sh
wrangler r2 bucket list
```
After running the `list` command, you will see all bucket names, including the ones you have just created.
## 4. Configure your Wrangler configuration file
Configure your `browser-worker` project's [Wrangler configuration file](/workers/wrangler/configuration/) by adding a browser [binding](/workers/runtime-apis/bindings/) and a [Node.js compatibility flag](/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Browser bindings allow for communication between a Worker and a headless browser which allows you to do actions such as taking a screenshot, generating a PDF and more.
Update your Wrangler configuration file with the Browser Rendering API binding, the R2 bucket you created and a Durable Object:
```toml
name = "rendering-api-demo"
main = "src/index.js"
compatibility_date = "2023-09-04"
compatibility_flags = [ "nodejs_compat"]
account_id = ""
# Browser Rendering API binding
browser = { binding = "MYBROWSER" }
# Bind an R2 Bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "screenshots"
preview_bucket_name = "screenshots-test"
# Binding to a Durable Object
[[durable_objects.bindings]]
name = "BROWSER"
class_name = "Browser"
[[migrations]]
tag = "v1" # Should be unique for each entry
new_sqlite_classes = ["Browser"] # Array of new classes
```
## 5. Code
The code below uses Durable Object to instantiate a browser using Puppeteer. It then opens a series of web pages with different resolutions, takes a screenshot of each, and uploads it to R2.
The Durable Object keeps a browser session open for 60 seconds after last use. If a browser session is open, any requests will re-use the existing session rather than creating a new one. Update your Worker code by copy and pasting the following:
```js
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
let id = env.BROWSER.idFromName("browser");
let obj = env.BROWSER.get(id);
// Send a request to the Durable Object, then await its response.
let resp = await obj.fetch(request.url);
return resp;
},
};
const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;
export class Browser {
constructor(state, env) {
this.state = state;
this.env = env;
this.keptAliveInSeconds = 0;
this.storage = this.state.storage;
}
async fetch(request) {
// screen resolutions to test out
const width = [1920, 1366, 1536, 360, 414];
const height = [1080, 768, 864, 640, 896];
// use the current date and time to create a folder structure for R2
const nowDate = new Date();
var coeff = 1000 * 60 * 5;
var roundedDate = new Date(
Math.round(nowDate.getTime() / coeff) * coeff,
).toString();
var folder = roundedDate.split(" GMT")[0];
//if there's a browser session open, re-use it
if (!this.browser || !this.browser.isConnected()) {
console.log(`Browser DO: Starting new instance`);
try {
this.browser = await puppeteer.launch(this.env.MYBROWSER);
} catch (e) {
console.log(
`Browser DO: Could not start browser instance. Error: ${e}`,
);
}
}
// Reset keptAlive after each call to the DO
this.keptAliveInSeconds = 0;
const page = await this.browser.newPage();
// take screenshots of each screen size
for (let i = 0; i < width.length; i++) {
await page.setViewport({ width: width[i], height: height[i] });
await page.goto("https://workers.cloudflare.com/");
const fileName = "screenshot_" + width[i] + "x" + height[i];
const sc = await page.screenshot();
await this.env.BUCKET.put(folder + "/" + fileName + ".jpg", sc);
}
// Close tab when there is no more work to be done on the page
await page.close();
// Reset keptAlive after performing tasks to the DO.
this.keptAliveInSeconds = 0;
// set the first alarm to keep DO alive
let currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
console.log(`Browser DO: setting alarm`);
const TEN_SECONDS = 10 * 1000;
await this.storage.setAlarm(Date.now() + TEN_SECONDS);
}
return new Response("success");
}
async alarm() {
this.keptAliveInSeconds += 10;
// Extend browser DO life
if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {
console.log(
`Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,
);
await this.storage.setAlarm(Date.now() + 10 * 1000);
// You could ensure the ws connection is kept alive by requesting something
// or just let it close automatically when there is no work to be done
// for example, `await this.browser.version()`
} else {
console.log(
`Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,
);
if (this.browser) {
console.log(`Closing browser.`);
await this.browser.close();
}
}
}
}
```
## 6. Test
Run [`npx wrangler dev --remote`](/workers/wrangler/commands/#dev) to test your Worker remotely before deploying to Cloudflare's global network. Local mode support does not exist for Browser Rendering so `--remote` is required.
## 7. Deploy
Run [`npx wrangler deploy`](/workers/wrangler/commands/#deploy) to deploy your Worker to the Cloudflare global network.
## Related resources
- Other [Puppeteer examples](https://github.com/cloudflare/puppeteer/tree/main/examples)
- Get started with [Durable Objects](/durable-objects/get-started/)
- [Using R2 from Workers](/r2/api/workers/workers-api-usage/)
---
# Reuse sessions
URL: https://developers.cloudflare.com/browser-rendering/workers-binding-api/reuse-sessions/
import { Render, PackageManagers, WranglerConfig } from "~/components";
The best way to improve the performance of your browser rendering Worker is to reuse sessions. One way to do that is via [Durable Objects](/browser-rendering/workers-binding-api/browser-rendering-with-do/), which allows you to keep a long running connection from a Worker to a browser. Another way is to keep the browser open after you've finished with it, and connect to that session each time you have a new request.
In short, this entails using `browser.disconnect()` instead of `browser.close()`, and, if there are available sessions, using `puppeteer.connect(env.MY_BROWSER, sessionID)` instead of launching a new browser session.
## 1. Create a Worker project
[Cloudflare Workers](/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.
Create a new Worker project named `browser-worker` by running:
## 2. Install Puppeteer
In your `browser-worker` directory, install Cloudflare's [fork of Puppeteer](/browser-rendering/platform/puppeteer/):
```sh
npm install @cloudflare/puppeteer --save-dev
```
## 3. Configure the [Wrangler configuration file](/workers/wrangler/configuration/)
```toml
name = "browser-worker"
main = "src/index.ts"
compatibility_date = "2023-03-14"
compatibility_flags = [ "nodejs_compat" ]
browser = { binding = "MYBROWSER" }
```
## 4. Code
The script below starts by fetching the current running sessions. If there are any that don't already have a worker connection, it picks a random session ID and attempts to connect (`puppeteer.connect(..)`) to it. If that fails or there were no running sessions to start with, it launches a new browser session (`puppeteer.launch(..)`). Then, it goes to the website and fetches the dom. Once that's done, it disconnects (`browser.disconnect()`), making the connection available to other workers.
Take into account that if the browser is idle, i.e. does not get any command, for more than the current [limit](/browser-rendering/platform/limits/), it will close automatically, so you must have enough requests per minute to keep it alive.
```ts
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise {
const url = new URL(request.url);
let reqUrl = url.searchParams.get("url") || "https://example.com";
reqUrl = new URL(reqUrl).toString(); // normalize
// Pick random session from open sessions
let sessionId = await this.getRandomSession(env.MYBROWSER);
let browser, launched;
if (sessionId) {
try {
browser = await puppeteer.connect(env.MYBROWSER, sessionId);
} catch (e) {
// another worker may have connected first
console.log(`Failed to connect to ${sessionId}. Error ${e}`);
}
}
if (!browser) {
// No open sessions, launch new session
browser = await puppeteer.launch(env.MYBROWSER);
launched = true;
}
sessionId = browser.sessionId(); // get current session id
// Do your work here
const page = await browser.newPage();
const response = await page.goto(reqUrl);
const html = await response!.text();
// All work done, so free connection (IMPORTANT!)
browser.disconnect();
return new Response(
`${launched ? "Launched" : "Connected to"} ${sessionId} \n-----\n` + html,
{
headers: {
"content-type": "text/plain",
},
},
);
},
// Pick random free session
// Other custom logic could be used instead
async getRandomSession(endpoint: puppeteer.BrowserWorker): Promise {
const sessions: puppeteer.ActiveSession[] =
await puppeteer.sessions(endpoint);
console.log(`Sessions: ${JSON.stringify(sessions)}`);
const sessionsIds = sessions
.filter((v) => {
return !v.connectionId; // remove sessions with workers connected to them
})
.map((v) => {
return v.sessionId;
});
if (sessionsIds.length === 0) {
return;
}
const sessionId =
sessionsIds[Math.floor(Math.random() * sessionsIds.length)];
return sessionId!;
},
};
```
Besides `puppeteer.sessions()`, we've added other methods to facilitate [Session Management](/browser-rendering/platform/puppeteer/#session-management).
## 5. Test
Run [`npx wrangler dev --remote`](/workers/wrangler/commands/#dev) to test your Worker remotely before deploying to Cloudflare's global network. Local mode support does not exist for Browser Rendering so `--remote` is required.
To test go to the following URL:
`/?url=https://example.com`
## 6. Deploy
Run `npx wrangler deploy` to deploy your Worker to the Cloudflare global network and then to go to the following URL:
`..workers.dev/?url=https://example.com`
---
# Deploy a Browser Rendering Worker
URL: https://developers.cloudflare.com/browser-rendering/workers-binding-api/screenshots/
import { Render, TabItem, Tabs, PackageManagers, WranglerConfig } from "~/components";
By following this guide, you will create a Worker that uses the Browser Rendering API to take screenshots from web pages. This is a common use case for browser automation.
## 1. Create a Worker project
[Cloudflare Workers](/workers/) provides a serverless execution environment that allows you to create new applications or augment existing ones without configuring or maintaining infrastructure. Your Worker application is a container to interact with a headless browser to do actions, such as taking screenshots.
Create a new Worker project named `browser-worker` by running:
## 2. Install Puppeteer
In your `browser-worker` directory, install Cloudflare’s [fork of Puppeteer](/browser-rendering/platform/puppeteer/):
```sh
npm install @cloudflare/puppeteer --save-dev
```
## 3. Create a KV namespace
Browser Rendering can be used with other developer products. You might need a [relational database](/d1/), an [R2 bucket](/r2/) to archive your crawled pages and assets, a [Durable Object](/durable-objects/) to keep your browser instance alive and share it with multiple requests, or [Queues](/queues/) to handle your jobs asynchronous.
For the purpose of this guide, you are going to use a [KV store](/kv/concepts/kv-namespaces/) to cache your screenshots.
Create two namespaces, one for production, and one for development.
```sh
npx wrangler kv namespace create BROWSER_KV_DEMO
npx wrangler kv namespace create BROWSER_KV_DEMO --preview
```
Take note of the IDs for the next step.
## 4. Configure the Wrangler configuration file
Configure your `browser-worker` project's [Wrangler configuration file](/workers/wrangler/configuration/) by adding a browser [binding](/workers/runtime-apis/bindings/) and a [Node.js compatibility flag](/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Bindings allow your Workers to interact with resources on the Cloudflare developer platform. Your browser `binding` name is set by you, this guide uses the name `MYBROWSER`. Browser bindings allow for communication between a Worker and a headless browser which allows you to do actions such as taking a screenshot, generating a PDF and more.
Update your [Wrangler configuration file](/workers/wrangler/configuration/) with the Browser Rendering API binding and the KV namespaces you created:
```toml title="wrangler.toml"
name = "browser-worker"
main = "src/index.js"
compatibility_date = "2023-03-14"
compatibility_flags = [ "nodejs_compat" ]
browser = { binding = "MYBROWSER" }
kv_namespaces = [
{ binding = "BROWSER_KV_DEMO", id = "22cf855786094a88a6906f8edac425cd", preview_id = "e1f8b68b68d24381b57071445f96e623" }
]
```
## 5. Code
Update `src/index.js` with your Worker code:
```js
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const { searchParams } = new URL(request.url);
let url = searchParams.get("url");
let img;
if (url) {
url = new URL(url).toString(); // normalize
img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });
if (img === null) {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url);
img = await page.screenshot();
await env.BROWSER_KV_DEMO.put(url, img, {
expirationTtl: 60 * 60 * 24,
});
await browser.close();
}
return new Response(img, {
headers: {
"content-type": "image/jpeg",
},
});
} else {
return new Response("Please add an ?url=https://example.com/ parameter");
}
},
};
```
Update `src/index.ts` with your Worker code:
```ts
import puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
BROWSER_KV_DEMO: KVNamespace;
}
export default {
async fetch(request, env): Promise {
const { searchParams } = new URL(request.url);
let url = searchParams.get("url");
let img: Buffer;
if (url) {
url = new URL(url).toString(); // normalize
img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" });
if (img === null) {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url);
img = (await page.screenshot()) as Buffer;
await env.BROWSER_KV_DEMO.put(url, img, {
expirationTtl: 60 * 60 * 24,
});
await browser.close();
}
return new Response(img, {
headers: {
"content-type": "image/jpeg",
},
});
} else {
return new Response("Please add an ?url=https://example.com/ parameter");
}
},
} satisfies ExportedHandler;
```
This Worker instantiates a browser using Puppeteer, opens a new page, navigates to what you put in the `"url"` parameter, takes a screenshot of the page, stores the screenshot in KV, closes the browser, and responds with the JPEG image of the screenshot.
If your Worker is running in production, it will store the screenshot to the production KV namespace. If you are running `wrangler dev`, it will store the screenshot to the dev KV namespace.
If the same `"url"` is requested again, it will use the cached version in KV instead, unless it expired.
## 6. Test
Run [`npx wrangler dev --remote`](/workers/wrangler/commands/#dev) to test your Worker remotely before deploying to Cloudflare's global network. Local mode support does not exist for Browser Rendering so `--remote` is required.
To test taking your first screenshot, go to the following URL:
`/?url=https://example.com`
## 7. Deploy
Run `npx wrangler deploy` to deploy your Worker to the Cloudflare global network.
To take your first screenshot, go to the following URL:
`..workers.dev/?url=https://example.com`
## Related resources
- Other [Puppeteer examples](https://github.com/cloudflare/puppeteer/tree/main/examples)
---