Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost
Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost
Jul 20, 2025
Jul 20, 2025
/
San Francisco
/

Nikola Balic
Nikola Balic

Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost Your Puppeteer script works flawlessly on your MacBook. Push to prod? Instant Cloudflare ban. Install headless Chrome on your server? Welcome to dependency hell. Steel gives you cloud browsers that just work; in Node, Deno, Bun, or wherever JavaScript runs these days.
When localhost:9222 Isn't Enough Anymore
Picture this: It's 3am. Your e-commerce scraper is down. Again. The Docker container crashed because Chrome ate all the RAM. Your proxy provider ghosted you. The site you're scraping now requires you to solve a picture puzzle of traffic lights.
You open GitHub and see 47 open issues on puppeteer-extra-plugin-stealth. The last commit was 8 months ago.
There has to be a better way.
Enter Steel: Cloud browsers with a proper API. No containers. No plugins. Just browsers that don't get detected.
Zero to Browser in 30 Seconds
npmnpmnpmnpmNo post-install scripts downloading 300MB of Chromium. No "please install these fonts for emoji support." Just a lightweight SDK with minimal dependencies.
Here's your first cloud browser:
import Steel from 'steel-sdk'; async function main() { try { const steel = new Steel(); // reads STEEL_API_KEY from env const session = await steel.sessions.create(); console.log(`Session ID: ${session.id}`); console.log(`Watch live: ${session.sessionViewerUrl}`); console.log(`WebSocket: ${session.websocketUrl}`); // That's it. You have a browser. In the cloud. Ready to connect. // Always clean up await steel.sessions.release(session.id); } catch (error) { console.error('Error:', error.message); } } main();
import Steel from 'steel-sdk'; async function main() { try { const steel = new Steel(); // reads STEEL_API_KEY from env const session = await steel.sessions.create(); console.log(`Session ID: ${session.id}`); console.log(`Watch live: ${session.sessionViewerUrl}`); console.log(`WebSocket: ${session.websocketUrl}`); // That's it. You have a browser. In the cloud. Ready to connect. // Always clean up await steel.sessions.release(session.id); } catch (error) { console.error('Error:', error.message); } } main();
import Steel from 'steel-sdk'; async function main() { try { const steel = new Steel(); // reads STEEL_API_KEY from env const session = await steel.sessions.create(); console.log(`Session ID: ${session.id}`); console.log(`Watch live: ${session.sessionViewerUrl}`); console.log(`WebSocket: ${session.websocketUrl}`); // That's it. You have a browser. In the cloud. Ready to connect. // Always clean up await steel.sessions.release(session.id); } catch (error) { console.error('Error:', error.message); } } main();
import Steel from 'steel-sdk'; async function main() { try { const steel = new Steel(); // reads STEEL_API_KEY from env const session = await steel.sessions.create(); console.log(`Session ID: ${session.id}`); console.log(`Watch live: ${session.sessionViewerUrl}`); console.log(`WebSocket: ${session.websocketUrl}`); // That's it. You have a browser. In the cloud. Ready to connect. // Always clean up await steel.sessions.release(session.id); } catch (error) { console.error('Error:', error.message); } } main();
The TypeScript Experience We Deserve
Remember wrestling with Puppeteer's types? Page | null | undefined | ElementHandle<Element | null> | void?
Steel's SDK is TypeScript-first with types that actually help:
import Steel from 'steel-sdk'; import type { Session, SessionCreateParams } from 'steel-sdk'; // Every option, fully typed const config: SessionCreateParams = { dimensions: { // ā viewport config width: 1920, height: 1080 }, timeout: 600000, // ā session duration (10 min) sessionContext: { // ā preload cookies & storage cookies: [/* ... */], localStorage: {/* ... */} }, // Pro features (when you upgrade): useProxy: true, // residential proxies solveCaptcha: true, // beats reCAPTCHA region: 'us-east' // geographic targeting }; // Autocomplete everywhere const session = await steel.sessions.create(config); session.// ā Your IDE knows everything
import Steel from 'steel-sdk'; import type { Session, SessionCreateParams } from 'steel-sdk'; // Every option, fully typed const config: SessionCreateParams = { dimensions: { // ā viewport config width: 1920, height: 1080 }, timeout: 600000, // ā session duration (10 min) sessionContext: { // ā preload cookies & storage cookies: [/* ... */], localStorage: {/* ... */} }, // Pro features (when you upgrade): useProxy: true, // residential proxies solveCaptcha: true, // beats reCAPTCHA region: 'us-east' // geographic targeting }; // Autocomplete everywhere const session = await steel.sessions.create(config); session.// ā Your IDE knows everything
import Steel from 'steel-sdk'; import type { Session, SessionCreateParams } from 'steel-sdk'; // Every option, fully typed const config: SessionCreateParams = { dimensions: { // ā viewport config width: 1920, height: 1080 }, timeout: 600000, // ā session duration (10 min) sessionContext: { // ā preload cookies & storage cookies: [/* ... */], localStorage: {/* ... */} }, // Pro features (when you upgrade): useProxy: true, // residential proxies solveCaptcha: true, // beats reCAPTCHA region: 'us-east' // geographic targeting }; // Autocomplete everywhere const session = await steel.sessions.create(config); session.// ā Your IDE knows everything
import Steel from 'steel-sdk'; import type { Session, SessionCreateParams } from 'steel-sdk'; // Every option, fully typed const config: SessionCreateParams = { dimensions: { // ā viewport config width: 1920, height: 1080 }, timeout: 600000, // ā session duration (10 min) sessionContext: { // ā preload cookies & storage cookies: [/* ... */], localStorage: {/* ... */} }, // Pro features (when you upgrade): useProxy: true, // residential proxies solveCaptcha: true, // beats reCAPTCHA region: 'us-east' // geographic targeting }; // Autocomplete everywhere const session = await steel.sessions.create(config); session.// ā Your IDE knows everything
No more @ts-ignore. No more "I hope this property exists." Just types that work.
Puppeteer, But Make It Cloud Native
Your existing Puppeteer code? It just works:
import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer import Steel from 'steel-sdk'; async function scrapeWithStyle() { const steel = new Steel(); // Get a cloud browser with all protections const session = await steel.sessions.create({ useProxy: true, solveCaptcha: true }); // Connect Puppeteer like nothing changed const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); // Your code runs exactly the same const page = await browser.newPage(); await page.goto('https://bot-protected-site.com'); // Except now it doesn't get blocked š const data = await page.evaluate(() => { return document.querySelector('.price')?.textContent; }); // Always clean up await steel.sessions.release(session.id); return data; }
import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer import Steel from 'steel-sdk'; async function scrapeWithStyle() { const steel = new Steel(); // Get a cloud browser with all protections const session = await steel.sessions.create({ useProxy: true, solveCaptcha: true }); // Connect Puppeteer like nothing changed const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); // Your code runs exactly the same const page = await browser.newPage(); await page.goto('https://bot-protected-site.com'); // Except now it doesn't get blocked š const data = await page.evaluate(() => { return document.querySelector('.price')?.textContent; }); // Always clean up await steel.sessions.release(session.id); return data; }
import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer import Steel from 'steel-sdk'; async function scrapeWithStyle() { const steel = new Steel(); // Get a cloud browser with all protections const session = await steel.sessions.create({ useProxy: true, solveCaptcha: true }); // Connect Puppeteer like nothing changed const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); // Your code runs exactly the same const page = await browser.newPage(); await page.goto('https://bot-protected-site.com'); // Except now it doesn't get blocked š const data = await page.evaluate(() => { return document.querySelector('.price')?.textContent; }); // Always clean up await steel.sessions.release(session.id); return data; }
import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer import Steel from 'steel-sdk'; async function scrapeWithStyle() { const steel = new Steel(); // Get a cloud browser with all protections const session = await steel.sessions.create({ useProxy: true, solveCaptcha: true }); // Connect Puppeteer like nothing changed const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); // Your code runs exactly the same const page = await browser.newPage(); await page.goto('https://bot-protected-site.com'); // Except now it doesn't get blocked š const data = await page.evaluate(() => { return document.querySelector('.price')?.textContent; }); // Always clean up await steel.sessions.release(session.id); return data; }
Same API. Better infrastructure. Zero learning curve.
Works With Your Entire Stack
Playwright? Obviously.
import { chromium } from 'playwright'; import Steel from 'steel-sdk'; async function usePlaywright() { const steel = new Steel(); const session = await steel.sessions.create(); try { const browser = await chromium.connectOverCDP(session.websocketUrl); const context = await browser.newContext(); const page = await context.newPage(); // Now use Playwright as normal await page.goto('https://example.com'); const title = await page.title(); console.log(`Title: ${title}`); await browser.close(); } finally { await steel.sessions.release(session.id); } }
import { chromium } from 'playwright'; import Steel from 'steel-sdk'; async function usePlaywright() { const steel = new Steel(); const session = await steel.sessions.create(); try { const browser = await chromium.connectOverCDP(session.websocketUrl); const context = await browser.newContext(); const page = await context.newPage(); // Now use Playwright as normal await page.goto('https://example.com'); const title = await page.title(); console.log(`Title: ${title}`); await browser.close(); } finally { await steel.sessions.release(session.id); } }
import { chromium } from 'playwright'; import Steel from 'steel-sdk'; async function usePlaywright() { const steel = new Steel(); const session = await steel.sessions.create(); try { const browser = await chromium.connectOverCDP(session.websocketUrl); const context = await browser.newContext(); const page = await context.newPage(); // Now use Playwright as normal await page.goto('https://example.com'); const title = await page.title(); console.log(`Title: ${title}`); await browser.close(); } finally { await steel.sessions.release(session.id); } }
import { chromium } from 'playwright'; import Steel from 'steel-sdk'; async function usePlaywright() { const steel = new Steel(); const session = await steel.sessions.create(); try { const browser = await chromium.connectOverCDP(session.websocketUrl); const context = await browser.newContext(); const page = await context.newPage(); // Now use Playwright as normal await page.goto('https://example.com'); const title = await page.title(); console.log(`Title: ${title}`); await browser.close(); } finally { await steel.sessions.release(session.id); } }
Running on Vercel Edge? Cloudflare Workers? No problem.
// edge-function.ts import Steel from 'steel-sdk'; export default async function handler(req: Request) { const steel = new Steel({ steelAPIKey: process.env.STEEL_API_KEY }); const session = await steel.sessions.create(); // Steel handles the WebSocket ā HTTP bridge automatically return Response.json({ sessionId: session.id, viewerUrl: session.sessionViewerUrl }); }
// edge-function.ts import Steel from 'steel-sdk'; export default async function handler(req: Request) { const steel = new Steel({ steelAPIKey: process.env.STEEL_API_KEY }); const session = await steel.sessions.create(); // Steel handles the WebSocket ā HTTP bridge automatically return Response.json({ sessionId: session.id, viewerUrl: session.sessionViewerUrl }); }
// edge-function.ts import Steel from 'steel-sdk'; export default async function handler(req: Request) { const steel = new Steel({ steelAPIKey: process.env.STEEL_API_KEY }); const session = await steel.sessions.create(); // Steel handles the WebSocket ā HTTP bridge automatically return Response.json({ sessionId: session.id, viewerUrl: session.sessionViewerUrl }); }
// edge-function.ts import Steel from 'steel-sdk'; export default async function handler(req: Request) { const steel = new Steel({ steelAPIKey: process.env.STEEL_API_KEY }); const session = await steel.sessions.create(); // Steel handles the WebSocket ā HTTP bridge automatically return Response.json({ sessionId: session.id, viewerUrl: session.sessionViewerUrl }); }
Bun? Deno? We don't discriminate.
// Works in Bun import Steel from 'steel-sdk'; // Works in Deno import Steel from 'npm:steel-sdk'; // Same API, same features, everywhere
// Works in Bun import Steel from 'steel-sdk'; // Works in Deno import Steel from 'npm:steel-sdk'; // Same API, same features, everywhere
// Works in Bun import Steel from 'steel-sdk'; // Works in Deno import Steel from 'npm:steel-sdk'; // Same API, same features, everywhere
// Works in Bun import Steel from 'steel-sdk'; // Works in Deno import Steel from 'npm:steel-sdk'; // Same API, same features, everywhere
Session State That Persists (Like a Real Browser)
Remember implementing your own cookie jar? Managing localStorage across requests? Rebuilding browser state from scratch?
Steel sessions are stateful by default:
// Create a session with existing context const session = await steel.sessions.create({ sessionContext: { cookies: savedCookies, localStorage: { 'example.com': { 'auth_token': 'abc123', 'user_prefs': JSON.stringify({ theme: 'dark' }) } } } }); // ... do your thing ... // Save the context for next time const context = await steel.sessions.context(session.id); await saveToDatabase({ cookies: context.cookies, localStorage: context.localStorage, sessionStorage: context.sessionStorage });
// Create a session with existing context const session = await steel.sessions.create({ sessionContext: { cookies: savedCookies, localStorage: { 'example.com': { 'auth_token': 'abc123', 'user_prefs': JSON.stringify({ theme: 'dark' }) } } } }); // ... do your thing ... // Save the context for next time const context = await steel.sessions.context(session.id); await saveToDatabase({ cookies: context.cookies, localStorage: context.localStorage, sessionStorage: context.sessionStorage });
// Create a session with existing context const session = await steel.sessions.create({ sessionContext: { cookies: savedCookies, localStorage: { 'example.com': { 'auth_token': 'abc123', 'user_prefs': JSON.stringify({ theme: 'dark' }) } } } }); // ... do your thing ... // Save the context for next time const context = await steel.sessions.context(session.id); await saveToDatabase({ cookies: context.cookies, localStorage: context.localStorage, sessionStorage: context.sessionStorage });
// Create a session with existing context const session = await steel.sessions.create({ sessionContext: { cookies: savedCookies, localStorage: { 'example.com': { 'auth_token': 'abc123', 'user_prefs': JSON.stringify({ theme: 'dark' }) } } } }); // ... do your thing ... // Save the context for next time const context = await steel.sessions.context(session.id); await saveToDatabase({ cookies: context.cookies, localStorage: context.localStorage, sessionStorage: context.sessionStorage });
Sessions last up to 24 hours. Perfect for long-running automations, multi-step workflows, or just keeping that login alive.
When Your Automation Needs Files
Upload CSVs, download reports, handle PDFs, without the fs.writeFileSync dance:
// Upload a file to the session const csv = await steel.sessions.files.upload(session.id, { file: Buffer.from('name,email\njohn,john@example.com'), path: 'data/users.csv' }); // Your browser can now access it at file:///data/users.csv await page.goto('file:///data/users.csv'); // Download files the browser created const screenshot = await page.screenshot(); await steel.sessions.files.upload(session.id, { file: screenshot, path: 'screenshots/homepage.png' }); // Get everything as a zip const archive = await steel.sessions.files.downloadArchive(session.id);
// Upload a file to the session const csv = await steel.sessions.files.upload(session.id, { file: Buffer.from('name,email\njohn,john@example.com'), path: 'data/users.csv' }); // Your browser can now access it at file:///data/users.csv await page.goto('file:///data/users.csv'); // Download files the browser created const screenshot = await page.screenshot(); await steel.sessions.files.upload(session.id, { file: screenshot, path: 'screenshots/homepage.png' }); // Get everything as a zip const archive = await steel.sessions.files.downloadArchive(session.id);
// Upload a file to the session const csv = await steel.sessions.files.upload(session.id, { file: Buffer.from('name,email\njohn,john@example.com'), path: 'data/users.csv' }); // Your browser can now access it at file:///data/users.csv await page.goto('file:///data/users.csv'); // Download files the browser created const screenshot = await page.screenshot(); await steel.sessions.files.upload(session.id, { file: screenshot, path: 'screenshots/homepage.png' }); // Get everything as a zip const archive = await steel.sessions.files.downloadArchive(session.id);
// Upload a file to the session const csv = await steel.sessions.files.upload(session.id, { file: Buffer.from('name,email\njohn,john@example.com'), path: 'data/users.csv' }); // Your browser can now access it at file:///data/users.csv await page.goto('file:///data/users.csv'); // Download files the browser created const screenshot = await page.screenshot(); await steel.sessions.files.upload(session.id, { file: screenshot, path: 'screenshots/homepage.png' }); // Get everything as a zip const archive = await steel.sessions.files.downloadArchive(session.id);
No temp directories. No cleanup scripts. Just files that work.
Error Handling for the Real World
Because UnhandledPromiseRejectionWarning: Error: Protocol error (Target.createTarget): Target closed isn't helpful:
import Steel from 'steel-sdk'; class ResilientScraper { private steel: Steel; constructor() { this.steel = new Steel(); } async scrapeWithRetry(url: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const session = await this.createSessionWithBackoff(attempt); try { const result = await this.performScrape(session, url); return result; } catch (error) { // Actual useful error types if (error instanceof Steel.RateLimitError) { console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`); await this.wait(parseInt(error.headers['retry-after']) * 1000); } else if (error instanceof Steel.APIConnectionTimeoutError) { console.log(`Timeout on attempt ${attempt}, retrying...`); } else if (error instanceof Steel.BadRequestError) { // Don't retry invalid requests throw new Error(`Invalid request: ${error.message}`); } } finally { // Always cleanup await this.steel.sessions.release(session.id).catch(() => {}); } } throw new Error(`Failed after ${maxAttempts} attempts`); } private async createSessionWithBackoff(attempt: number) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); if (attempt > 1) await this.wait(delay); return this.steel.sessions.create({ useProxy: true, solveCaptcha: true }); } private wait = (ms: number) => new Promise(r => setTimeout(r, ms)); }
import Steel from 'steel-sdk'; class ResilientScraper { private steel: Steel; constructor() { this.steel = new Steel(); } async scrapeWithRetry(url: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const session = await this.createSessionWithBackoff(attempt); try { const result = await this.performScrape(session, url); return result; } catch (error) { // Actual useful error types if (error instanceof Steel.RateLimitError) { console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`); await this.wait(parseInt(error.headers['retry-after']) * 1000); } else if (error instanceof Steel.APIConnectionTimeoutError) { console.log(`Timeout on attempt ${attempt}, retrying...`); } else if (error instanceof Steel.BadRequestError) { // Don't retry invalid requests throw new Error(`Invalid request: ${error.message}`); } } finally { // Always cleanup await this.steel.sessions.release(session.id).catch(() => {}); } } throw new Error(`Failed after ${maxAttempts} attempts`); } private async createSessionWithBackoff(attempt: number) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); if (attempt > 1) await this.wait(delay); return this.steel.sessions.create({ useProxy: true, solveCaptcha: true }); } private wait = (ms: number) => new Promise(r => setTimeout(r, ms)); }
import Steel from 'steel-sdk'; class ResilientScraper { private steel: Steel; constructor() { this.steel = new Steel(); } async scrapeWithRetry(url: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const session = await this.createSessionWithBackoff(attempt); try { const result = await this.performScrape(session, url); return result; } catch (error) { // Actual useful error types if (error instanceof Steel.RateLimitError) { console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`); await this.wait(parseInt(error.headers['retry-after']) * 1000); } else if (error instanceof Steel.APIConnectionTimeoutError) { console.log(`Timeout on attempt ${attempt}, retrying...`); } else if (error instanceof Steel.BadRequestError) { // Don't retry invalid requests throw new Error(`Invalid request: ${error.message}`); } } finally { // Always cleanup await this.steel.sessions.release(session.id).catch(() => {}); } } throw new Error(`Failed after ${maxAttempts} attempts`); } private async createSessionWithBackoff(attempt: number) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); if (attempt > 1) await this.wait(delay); return this.steel.sessions.create({ useProxy: true, solveCaptcha: true }); } private wait = (ms: number) => new Promise(r => setTimeout(r, ms)); }
import Steel from 'steel-sdk'; class ResilientScraper { private steel: Steel; constructor() { this.steel = new Steel(); } async scrapeWithRetry(url: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const session = await this.createSessionWithBackoff(attempt); try { const result = await this.performScrape(session, url); return result; } catch (error) { // Actual useful error types if (error instanceof Steel.RateLimitError) { console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`); await this.wait(parseInt(error.headers['retry-after']) * 1000); } else if (error instanceof Steel.APIConnectionTimeoutError) { console.log(`Timeout on attempt ${attempt}, retrying...`); } else if (error instanceof Steel.BadRequestError) { // Don't retry invalid requests throw new Error(`Invalid request: ${error.message}`); } } finally { // Always cleanup await this.steel.sessions.release(session.id).catch(() => {}); } } throw new Error(`Failed after ${maxAttempts} attempts`); } private async createSessionWithBackoff(attempt: number) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); if (attempt > 1) await this.wait(delay); return this.steel.sessions.create({ useProxy: true, solveCaptcha: true }); } private wait = (ms: number) => new Promise(r => setTimeout(r, ms)); }
Real error types. Real retry strategies. Real production code.
The Features That Make You Go "Finally!"
Geographic Precision
const session = await steel.sessions.create({ useProxy: { geolocation: { country: 'US', state: 'NY', city: 'NEW_YORK' } } }); // Your browser is now a New Yorker š½
const session = await steel.sessions.create({ useProxy: { geolocation: { country: 'US', state: 'NY', city: 'NEW_YORK' } } }); // Your browser is now a New Yorker š½
const session = await steel.sessions.create({ useProxy: { geolocation: { country: 'US', state: 'NY', city: 'NEW_YORK' } } }); // Your browser is now a New Yorker š½
const session = await steel.sessions.create({ useProxy: { geolocation: { country: 'US', state: 'NY', city: 'NEW_YORK' } } }); // Your browser is now a New Yorker š½
Human-Like Behavior
const session = await steel.sessions.create({ stealthConfig: { humanizeInteractions: true // Mouse movements that don't scream "BOT" } });
const session = await steel.sessions.create({ stealthConfig: { humanizeInteractions: true // Mouse movements that don't scream "BOT" } });
const session = await steel.sessions.create({ stealthConfig: { humanizeInteractions: true // Mouse movements that don't scream "BOT" } });
const session = await steel.sessions.create({ stealthConfig: { humanizeInteractions: true // Mouse movements that don't scream "BOT" } });
Auto-CAPTCHA Solving
// Encounters reCAPTCHA? No problem. const session = await steel.sessions.create({ solveCaptcha: true }); // Check CAPTCHA status const status = await steel.sessions.captchas.status(session.id);
// Encounters reCAPTCHA? No problem. const session = await steel.sessions.create({ solveCaptcha: true }); // Check CAPTCHA status const status = await steel.sessions.captchas.status(session.id);
// Encounters reCAPTCHA? No problem. const session = await steel.sessions.create({ solveCaptcha: true }); // Check CAPTCHA status const status = await steel.sessions.captchas.status(session.id);
// Encounters reCAPTCHA? No problem. const session = await steel.sessions.create({ solveCaptcha: true }); // Check CAPTCHA status const status = await steel.sessions.captchas.status(session.id);
Session Inspection
// Watch your automation live (seriously, try this) console.log(`Debug live: ${session.sessionViewerUrl}`); // Get all session events for playback const events = await steel.sessions.events(session.id);
// Watch your automation live (seriously, try this) console.log(`Debug live: ${session.sessionViewerUrl}`); // Get all session events for playback const events = await steel.sessions.events(session.id);
// Watch your automation live (seriously, try this) console.log(`Debug live: ${session.sessionViewerUrl}`); // Get all session events for playback const events = await steel.sessions.events(session.id);
// Watch your automation live (seriously, try this) console.log(`Debug live: ${session.sessionViewerUrl}`); // Get all session events for playback const events = await steel.sessions.events(session.id);
Pricing That Doesn't Require a VC Round
Hobby (Free): 100 browser hours/month
Perfect for side projects
Basic anti-detection
Community Discord access
Pro: Pay for what you use
Residential proxies
CAPTCHA solving
Geographic targeting
Priority support
Enterprise: Let's talk
Custom proxy pools
Dedicated infrastructure
SLAs that mean something
A Slack channel where we actually respond
Migration Guide: From Localhost to Cloud
Got an existing Puppeteer project? Here's your 5-minute migration:
Before (Your Current Setup)
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // ... your automation ... await browser.close();
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // ... your automation ... await browser.close();
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // ... your automation ... await browser.close();
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // ... your automation ... await browser.close();
After (With Steel)
import puppeteer from 'puppeteer-core'; import Steel from 'steel-sdk'; const steel = new Steel(); const session = await steel.sessions.create(); const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); // ... your automation (unchanged!) ... await steel.sessions.release(session.id);
import puppeteer from 'puppeteer-core'; import Steel from 'steel-sdk'; const steel = new Steel(); const session = await steel.sessions.create(); const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); // ... your automation (unchanged!) ... await steel.sessions.release(session.id);
import puppeteer from 'puppeteer-core'; import Steel from 'steel-sdk'; const steel = new Steel(); const session = await steel.sessions.create(); const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); // ... your automation (unchanged!) ... await steel.sessions.release(session.id);
import puppeteer from 'puppeteer-core'; import Steel from 'steel-sdk'; const steel = new Steel(); const session = await steel.sessions.create(); const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); // ... your automation (unchanged!) ... await steel.sessions.release(session.id);
That's it. Your code stays the same. Your success rate goes through the roof.
Join our Discord where people actually help each other instead of just posting memes (okay, we post memes too).
Let's Ship Something
# Install npm install steel-sdk puppeteer-core dotenv # Set your API key (get one at app.steel.dev) echo "STEEL_API_KEY=your_api_key_here" > .env # Run this quickstart
# Install npm install steel-sdk puppeteer-core dotenv # Set your API key (get one at app.steel.dev) echo "STEEL_API_KEY=your_api_key_here" > .env # Run this quickstart
# Install npm install steel-sdk puppeteer-core dotenv # Set your API key (get one at app.steel.dev) echo "STEEL_API_KEY=your_api_key_here" > .env # Run this quickstart
# Install npm install steel-sdk puppeteer-core dotenv # Set your API key (get one at app.steel.dev) echo "STEEL_API_KEY=your_api_key_here" > .env # Run this quickstart
// quickstart.ts import 'dotenv/config'; import Steel from 'steel-sdk'; import puppeteer from 'puppeteer-core'; const steel = new Steel(); async function main() { let session; try { // Create a session session = await steel.sessions.create(); console.log(` š Session ready! šŗ Watch live: ${session.sessionViewerUrl} š Session ID: ${session.id} `); // Connect and scrape const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); await page.goto('https://example.com'); const title = await page.title(); console.log(`\nā Scraped title: ${title}`); await browser.close(); } catch (error) { console.error('Error:', error); process.exit(1); } finally { // Always cleanup if (session) { await steel.sessions.release(session.id); console.log('\nš§¹ Session released. Happy scraping!'); } } } main();
// quickstart.ts import 'dotenv/config'; import Steel from 'steel-sdk'; import puppeteer from 'puppeteer-core'; const steel = new Steel(); async function main() { let session; try { // Create a session session = await steel.sessions.create(); console.log(` š Session ready! šŗ Watch live: ${session.sessionViewerUrl} š Session ID: ${session.id} `); // Connect and scrape const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); await page.goto('https://example.com'); const title = await page.title(); console.log(`\nā Scraped title: ${title}`); await browser.close(); } catch (error) { console.error('Error:', error); process.exit(1); } finally { // Always cleanup if (session) { await steel.sessions.release(session.id); console.log('\nš§¹ Session released. Happy scraping!'); } } } main();
// quickstart.ts import 'dotenv/config'; import Steel from 'steel-sdk'; import puppeteer from 'puppeteer-core'; const steel = new Steel(); async function main() { let session; try { // Create a session session = await steel.sessions.create(); console.log(` š Session ready! šŗ Watch live: ${session.sessionViewerUrl} š Session ID: ${session.id} `); // Connect and scrape const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); await page.goto('https://example.com'); const title = await page.title(); console.log(`\nā Scraped title: ${title}`); await browser.close(); } catch (error) { console.error('Error:', error); process.exit(1); } finally { // Always cleanup if (session) { await steel.sessions.release(session.id); console.log('\nš§¹ Session released. Happy scraping!'); } } } main();
// quickstart.ts import 'dotenv/config'; import Steel from 'steel-sdk'; import puppeteer from 'puppeteer-core'; const steel = new Steel(); async function main() { let session; try { // Create a session session = await steel.sessions.create(); console.log(` š Session ready! šŗ Watch live: ${session.sessionViewerUrl} š Session ID: ${session.id} `); // Connect and scrape const browser = await puppeteer.connect({ browserWSEndpoint: session.websocketUrl }); const page = await browser.newPage(); await page.goto('https://example.com'); const title = await page.title(); console.log(`\nā Scraped title: ${title}`); await browser.close(); } catch (error) { console.error('Error:', error); process.exit(1); } finally { // Always cleanup if (session) { await steel.sessions.release(session.id); console.log('\nš§¹ Session released. Happy scraping!'); } } } main();
Resources for the Curious
GitHub - Star it, fork it, break it, fix it
API Docs - Every endpoint, every option
Steel Cookbook - Copy-paste-ship recipes
System Status - We're transparent about uptime
The Part Where We Get Real
Browser automation is hard. It shouldn't be. You came here to build something cool, not to become a Docker expert or a proxy specialist.
Steel handles the infrastructure so you can focus on the fun part, building things that matter.
No more "works on my machine." No more random bans. No more 3am debugging sessions.
Just npm install steel-sdk and start shipping.
Ready? Grab your API key and let's build something the internet hasn't seen before.
P.S. - If you build something cool, tell us in Discord. We love seeing what you ship with Steel. Plus, we might feature it (with your permission) and send you swag.
All Systems Operational

