Skip to content

Playwright / browser

Use this when the site needs a real browser anyway — a SPA, a login flow, or content you can't get from raw HTML. If all you want is a clearance cookie, HTTP clients are far cheaper.

Why go through the browser

The point isn't that solving works better here — it's who sends the submission. In this approach Chrome owns the actual request: the native interstitial POST, the captcha GET, or the Akamai sensor XHR. That means it carries a genuine Chrome TLS fingerprint and the browser's own cookie jar, which some sites check independently of the challenge itself.

That property is the reason to accept the cost. See solving ≠ staying unblocked.

DataDome

Attach to a Playwright page, let the helper watch for a challenge, and it resolves once DataDome returns an accepted cookie. It handles the interstitial → captcha escalation for you.

typescript
import { chromium } from 'playwright';
import { solve } from '#src/datadome/solver.js';

const browser = await chromium.launch();
const context = await browser.newContext({ proxy: { server: proxy } });
const page = await context.newPage();

const result = await solve(page, { proxy, solverApiKey, solverUrl, url });
// -> { cookie, responseStatus, url }

// the page is now cleared; carry on driving it normally
await page.goto('https://www.grainger.com/category/…');

Under the hood it watches for the challenge, asks your container for the sensor values via POST /dd/solve, and hands the result back to the page so Chrome performs the submission.

Runnable version: src/datadome/grainger.ts in xhrdev/examples.

Akamai — the WebSocket session

Akamai works differently. Rather than one request/response, the solver keeps a stateful session open and streams you one sensor submission per round, each of which you relay through the browser and report back on.

typescript
import { solve } from '#src/akamai/solver.js';

await solve(page, { proxy, solverApiKey, solverUrl, timeout: 120_000, url });

Runnable versions: src/akamai/comcast.ts and src/akamai/ca-edd.ts.

The protocol

If you're implementing this yourself rather than using the example helper:

  1. Launch a browser and navigate to the target URL.
  2. Capture the challenge — intercept the Akamai sensor script source, grab the page HTML, and collect current cookies.
  3. Connect to ws://host:3000/akamai/session.
  4. Send init with the captured script, HTML, cookies, and URL.
  5. Relay each submission as a real XHR/fetch in the browser, and reply with submission_response carrying the status, body, and updated cookies.
  6. Watch for cookie_update with accepted: true — the _abck cookie is now valid.
  7. Close the socket and continue browsing with the accepted cookies.

Full message schemas, session TTL, and submission timeouts are in the Akamai API reference.

Session limits

A session expires after 5 minutes of inactivity (each submission_response resets the timer), and each individual submission must be answered within 30 seconds.

Keeping the identity consistent

The browser and the profile you send the solver have to agree. If you launch Chrome with one user agent and tell the solver something else, the solve gets rejected in a way that doesn't point at the cause.

The examples share a single profile.ts between the browser launch options and the solver payload for exactly this reason — worth copying that structure.

Cost

Be realistic about what this costs relative to the HTTP path: a browser process per session, plus the page loads. If you're running this at volume, pool contexts rather than launching a browser per job, and reserve the browser approach for the targets that actually need it.

Next