Drıftstack docs
Docs

Migrating from Browserless

You have a Browserless deployment (cloud or self-hosted) and want to scope a port to Driftstack without surprises. The core difference: Browserless hands you a remote Chrome to script over CDP (the Chrome DevTools Protocol — the wire protocol Puppeteer speaks); Driftstack is action-based — you send typed steps (navigate, tap, type, wait, extract) to an iPhone Safari session over HTTPS, and there is no /function-style endpoint that runs your JavaScript server-side.

On hosted Driftstack the session to use is an agent session: POST /v1/agent-sessions with the proxy_id of one of your saved proxies, then your own steps with POST /v1/agent-sessions/{id}/steps. The step-by-step calls on /v1/sessions do not run on hosted today (see Step-by-step operations on the hosted service).

Surface comparison

Browserless Driftstack
Connect Puppeteer to wss://chrome.browserless.io?token=… POST /v1/agent-sessions with a proxy_id, then send steps (navigate, interact, wait, extract, capture) with POST /v1/agent-sessions/{id}/steps. No CDP passthrough.
/function (POST a JS body, get JSON back) No direct equivalent. Express the flow as steps; reads go through an extract step, whose text comes back in the step’s summary.
/screenshot A capture step with capture: "screenshot"; fetch the image by its captureId with GET /v1/agent-sessions/{id}/captures/{captureId}.
Concurrency you meter yourself Tier-driven cap; going over returns a 429 with the stable concurrency-limit problem type — see Concurrency & backpressure.
Stealth flags / fingerprint plugins Device profiles (archetype in the API — the device, OS and browser version a session presents) plus saved profiles that you reuse across sessions.

The first port: replace the WebSocket

A typical Browserless integration is a Puppeteer script pointed at their WebSocket endpoint:

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://chrome.browserless.io?token=${TOKEN}`,
});
const page = await browser.newPage();
await page.goto('https://example.com');
const heading = await page.$eval('h1', (el) => el.textContent);
await browser.close();

On Driftstack the same flow is an agent session and one list of steps:

import { randomUUID } from 'node:crypto';
import { Driftstack } from '@driftstack/sdk';

const client = new Driftstack({ apiKey: process.env.DRIFTSTACK_API_KEY! });

// Every hosted session goes out through one of your saved proxies.
const session = await client.agentSessions.create(
  { mode: 'ai', proxy_id: process.env.DRIFTSTACK_PROXY_ID! },
  { idempotencyKey: randomUUID() },
);
try {
  // Wait up to two minutes for its browser: `status` is `active` from the start, so read `ready`.
  const readyBy = Date.now() + 120_000;
  while (Date.now() < readyBy) {
    const s = await client.agentSessions.get(session.id);
    if (s.status === 'closed') throw new Error('The session did not start: ' + s.closed_reason);
    if (s.ready !== false) break;
    await new Promise((resolve) => setTimeout(resolve, 2_000));
  }
  const run = await client.agentSessions.runSteps(
    session.id,
    [
      { kind: 'navigate', url: 'https://example.com' },
      { kind: 'extract', selector: 'h1' },
    ],
    { idempotencyKey: randomUUID() },
  );
  if (run.kind === 'plan-executed') {
    const read = run.results.find((r) => r.intent.kind === 'extract');
    console.log(read?.kind === 'success' ? read.summary : run.results); // the heading's text
  }
} finally {
  await client.agentSessions.close(session.id);
}

No server-side JS anywhere: a login + scrape is one list of steps ending in an extract step. Steps run as written, with no AI planning and none of the AI budget used.

Profile persistence: the differentiator

Browserless gives you a fresh browser per connection unless you manage your own cookie jar. Driftstack persists cookies, local storage, and IndexedDB server-side in a named profile:

const profile = await client.profiles.create({ name: 'evergreen-scraper' });

// Start a session on the profile, drive the login once, and close it:
// closing saves the profile's cookies and storage.
const session = await client.agentSessions.create(
  { mode: 'ai', proxy_id: process.env.DRIFTSTACK_PROXY_ID!, profile_id: profile.id },
  { idempotencyKey: randomUUID() },
);
try {
  // … wait for `ready`, then send the login steps with runSteps …
} finally {
  // Closing saves the signed-in cookies and storage to the profile.
  await client.agentSessions.close(session.id);
}

Later sessions bind the same state by passing profile_id on POST /v1/agent-sessions. A profile allows one live session at a time — a second create against it returns 409 profile-in-use naming the active session, so two runs can’t overwrite each other’s cookies. A profile snapshot is not a backup of that signed-in state: it keeps the profile’s settings, and a profile restored from one starts with no cookies, localStorage or IndexedDB. Full flow: Profile management.

Picking a tier

Concurrency is the sizing axis: each plan runs a set number of sessions at once (the full cap table), and Enterprise is sized by contract. Every plan includes the API. Plans and prices: driftstack.io/pricing.

What Driftstack deliberately doesn’t do

  • Raw WebSocket / CDP passthrough. The action endpoints are the contract; arbitrary protocol messages are blocked on purpose.
  • Server-side JS execution. No /function equivalent. Express the flow as actions, or do the computation client-side over an extract / dom_snapshot read.
  • Browser choice. Driftstack is iPhone Safari (WebKit) by product scope — it is not a Chrome or Firefox automation service.
  • Session video recordings. The API has none (the desktop app can save recordings on your computer); capture steps are the API’s observation tool.

Self-hosting, on the other hand, does exist — as its own offering rather than a toggle on the cloud plans. See driftstack.io/self-hosted.

Migration checklist

  1. Inventory your Browserless calls — count /function vs /screenshot vs raw-CDP usage.
  2. If /function bodies do non-trivial in-page computation, or raw CDP is a large share of your traffic, email [email protected] before starting — the action surface may not cover you yet, and it’s cheaper to learn that first.
  3. Port one job with the TypeScript, Python, or Go SDK.
  4. Subscribe to webhooks for session.challenge_detected (a bot check paused a session) and session.profile_save_failed. An agent session sends no session.completed or session.failed; read its status and closed_reason to learn it ended.
  5. Check usage + quotas before flipping production traffic.

Need help porting?

Email [email protected] with your /function bodies and call volumes; that’s enough to scope whether the action surface covers the workload and which tier fits.