A real browser you drive, not a black box

Connect Playwright or Puppeteer to a fingerprinted Wayfern browser over CDP and keep the interaction, login state and exit control a scraping endpoint cannot hand back.

A response is not a browser

A scraping endpoint hands you the page it decided to fetch. A browser lets you log in, fill a form, wait for the thing you actually came for, and then keep going. The difference is structural, not a matter of features — so the only real question is who runs the browser.

Interaction
Scraping API
One request in, one response out
Self-hosted
Full CDP — once you are running the browsers
Wayfern
Full CDP over one WebSocket: goto, fill, click, wait
Login state
Scraping API
Ends with the response
Self-hosted
Yours to keep alive
Wayfern
Cookies and storage stay live for as long as the socket is open
Isolation
Scraping API
Not exposed to the caller
Self-hosted
Yours to build and keep enforcing
Wayfern
A pooled instance is reused only for the same link token, workload and exit
Geo granularity
Scraping API
Whatever the vendor chooses to expose
Self-hosted
Whatever your proxy vendor exposes, wired by hand
Wayfern
Country → region → city → ISP, plus a sticky session id
Who maintains it
Scraping API
The vendor, on their roadmap
Self-hosted
You: pool, upgrades, proxy wiring, driver versions
Wayfern
We run the pool; you keep your Playwright code

Your driver, our browser

Open the CDP WebSocket at wss://browser.wayfern.com/ws with a link token in the query string. There is no SDK to install and nothing to learn: from the connect call onward it is the Playwright or Puppeteer you already write.

Log in, then keep going
import { chromium } from "playwright";

// The session is created when this socket opens — there is no REST call to
// "start" one, and no queue to poll.
const browser = await chromium.connectOverCDP(
  "wss://browser.wayfern.com/ws?token=<LINK_TOKEN>&country=US&region=CA&session=run42",
);
const page = await browser.contexts()[0].newPage();

// Everything below is ordinary Playwright, running on a browser you hold open.
await page.goto("https://example.com/login");
await page.fill("#email", process.env.ACCOUNT_EMAIL);
await page.fill("#password", process.env.ACCOUNT_PASSWORD);
await page.click("button[type=submit]");
await page.waitForSelector("[data-testid=dashboard]");

console.log(await page.title());
await browser.close();

The session starts with the socket

No create call, no session id to poll for. On a successful connection the gateway sends one JSON message, then proxies CDP between your driver and the browser for as long as the socket stays open.

First message on success
shell
{
  "type": "connected",
  "connectionId": "0c2c3a1e-...",
  "instanceId": "inst-...",
  "contextId": "ctx-..."
}

Your token, your instance

The pool only reuses a browser for connections that carry the same link token and the same exit location. A pooled instance is never shared with another token, another workload, or a different geo.

Down to the city and the carrier

Geo targeting is part of the connection string, not a separate proxy integration. Add country and the session egresses through a residential exit there; narrow it further with region, city and isp. Leave them off and you get the default datacenter exit, whose egress is free.

ParamTypeWhat it does
countrystringISO-3166 country code of the residential exit, e.g. "US". Required to enable residential location targeting; omit for the default exit. See GET /api/proxy/locations.
regionstringRegion/state code within the country, e.g. "CA". Optional; narrows the exit.
citystringCity code within the country/region. Optional; narrows the exit.
ispstringISP code. Optional; narrows the exit to a carrier.
sessionstringSticky-session id ([A-Za-z0-9]+). When set, the same residential exit IP is held for the session TTL so a multi-step flow keeps one IP.

Every code is checked against the live catalog before the browser is handed over. An unknown country, region, city or ISP is refused at the handshake with close code 4007 rather than silently falling back to the wrong location.

A sticky exit for multi-step flows

Add session=<id> and the same residential exit IP is held for the session TTL, so a login on step one and a checkout on step six are seen coming from one address.

Targeted connection URL
shell
wss://browser.wayfern.com/ws?token=<LINK_TOKEN>&country=US&region=CA&session=run42

Ask what you can target

The catalog is live, not a static list in a PDF. These endpoints take a dashboard JWT and return code / name pairs you can feed straight back into the connection string.

  • GET /api/proxy/locations
  • GET /api/proxy/locations/countries
  • GET /api/proxy/locations/regions?country=US
  • GET /api/proxy/locations/cities?country=US&region=CA
  • GET /api/proxy/locations/isps?country=US
Response: 200 OK
shell
{
  "locations": [
    { "code": "US", "name": "United States" },
    { "code": "GB", "name": "United Kingdom" }
  ]
}

Link tokens are the throttle

The token in the socket URL is not just authentication. It is the unit you cap, isolate and switch off with, so one crawler's connection limit can be lowered — or its token revoked — without touching any of the others.

One token per job, per project

Link tokens live inside a project and are created by its owners and admins, separately from your wf_ API tokens. Give each crawler its own so you can turn one off without touching the rest.

A concurrency cap you set

Every link carries a maxConcurrentConnections value between 1 and 100, defaulting to 10. Connection number cap+1 is refused at the handshake with close code 4003 rather than quietly queued.

A live count, not an estimate

Listing a project’s links returns activeConnections alongside each one — the number of sockets currently open on that token, read from the connection tracker.

Revoke, regenerate, expire

Revoke a leaked token and every new connection is refused with close code 4002. Regenerate issues a fresh secret on the same link. An optional ISO-8601 expiry retires a token on schedule; absent means it never expires.

Every refusal is typed

When the gateway will not give you a browser it says so in a way you can branch on: a JSON { "type": "error", "code", "message" } message, then a WebSocket close with one of these codes. No connection is left hanging for you to time out.

CodeMeaning
4001Authentication required: token missing.
4002Invalid or revoked token.
4003Rate limit exceeded: max N concurrent connections for this link.
4004No billing account for this link.
4005Session not allowed (billing gate, e.g. no active subscription or no remaining session-hours).
4006Residential proxy location control is not available on this deployment.
4007Invalid proxy location (unknown country/region/city/isp).
4008Failed to provision the residential proxy.
4500Internal server error.

Metered by the open page

You are charged for pages you hold open, from the same credit balance the Web Data API draws on. Nothing on this page is a separate line item on your invoice.

8
credits / open-page hour

One page held open for one hour, or $0.10 at the base credit price. Two pages for half an hour costs the same as one page for an hour.

60s
minimum per page

Every page you open is billed for at least 60 seconds, however fast you close it. We would rather say that here than bury it in an invoice.

320
credits / GB residential

Only when you ask for a residential exit, metered per byte as it is consumed. Datacenter egress — every session without location params — is free.

Run the balance down and the gateway refuses the next connection with close code 4005 instead of running up a bill. Full rates, plans and credit packs are on the pricing page.

Do not hold a session to read one page

Plenty of work is a single fetch with no clicking involved. For that, a session is the wrong shape and the wrong price: the Web Data API runs the same anti-detect browsers, hands back markdown, HTML, links and metadata, and bills per page instead of per hour.

Use a session when…

You need to log in, fill something, click through steps, wait on a selector, or hold one exit IP across a flow — anything where the next request depends on what the last one rendered.

Use the Web Data API when…

You just want the page. Scrape is 1 credit per page, crawl 1 per successful page, and a screenshot 5 per capture.

Mix them freely

Both draw on one credit balance, so discovering URLs with the Web Data API and then driving the handful that need a login over CDP needs no second plan and no second contract.

Ready to scale?

Create a project, mint a link token, and point your existing Playwright or Puppeteer script at the gateway.