Skip to main content
Beta Feature — Browser automation is currently in beta. Please report any issues to founders@morphllm.com.
Browser Testing Test your web apps with natural language. “Test checkout flow” or “Verify mobile menu works”—Morph runs it with a real browser.

Quick Start

Install and run your first browser test in 30 seconds:
import { MorphClient } from '@morphllm/morphsdk';

const morph = new MorphClient({ apiKey: "YOUR_API_KEY" });

const result = await morph.browser.execute({
  task: "Verify the homepage loads and has a working navigation menu",
  url: "https://example.com"
});

console.log(result.success ? '✅ Passed' : '❌ Failed');
console.log(result.result);
Use remote URLs only (Vercel previews, e2b.dev tunnels, staging). Cannot access localhost—see why.

Why Morph Browser?

Built specifically for testing AI-generated code changes:
  • 10x cheaper than Claude Sonnet (0.30vs0.30 vs 3.00 per 1M input tokens)
  • 250% faster inference (200 tok/s vs 60 tok/s)
  • Live session streaming - Watch tests execute in real-time
  • Rich debugging - URLs, actions, errors auto-captured
  • Agent self-assessment - Real success detection, not just completion
ModelInputOutputSpeedBest For
morph-computer-use-v1$0.30$1.50200 tok/sBrowser automation (default)
morph-computer-use-v0$0.30$1.50200 tok/sBrowser automation (legacy)
gemini-3-flash-preview$0.50$3.0090 tok/sExternal API (Google)
claude-sonnet-4.5$3.00$15.0060 tok/sGeneral reasoning
Per 1M tokens. Morph models are optimized for browser automation and testing.

Common Patterns

Basic Testing

Test any web flow with natural language:
const result = await morph.browser.execute({
  task: "Test the login flow with test@example.com / password123",
  url: "https://myapp.vercel.app"
});

if (result.success) {
  console.log('✅ Test passed');
} else {
  console.error('❌ Test failed:', result.error);
  console.log('Failed at:', result.urls[result.urls.length - 1]);
}

Watch Tests Live

Get live URL immediately, watch execution in real-time:
// Returns in ~2s with live URL
const task = await morph.browser.createTask({
  task: "Test checkout flow with cart persistence",
  url: "https://shop.example.com"
});

console.log('👁️  Watch live:', task.debugUrl);
// Open URL in browser to watch from start

// Wait for completion
const result = await task.complete();

Test Responsive Layouts

Use built-in tools to test different screen sizes:
const result = await morph.browser.execute({
  task: "Resize to mobile (375x667) and verify the hamburger menu appears",
  url: "https://myapp.com"
});

Site Authentication

Test sites that require login with the auth parameter:
const result = await morph.browser.execute({
  task: "Log in with x_user and x_pass, then verify the dashboard loads",
  url: "https://myapp.com/login",
  auth: {
    username: "test@example.com",
    password: "secret123"
  }
});
The agent sees placeholders (x_user, x_pass) in the task. Real values are injected when filling forms.
Scope credentials to specific domains:
const result = await morph.browser.execute({
  task: "Log in and test the integration",
  url: "https://staging.myapp.com",
  auth: {
    "https://*.staging.myapp.com": { username: "staging-user", password: "staging-pass" },
    "https://api.external.com": { username: "api-user", password: "api-key" }
  }
});

Browser Profiles (Persistent Logins)

Profiles let you sign in once (manually) and reuse that authenticated state across test runs. Profiles are scoped to a repo.

List available repos

const repos = await morph.browser.profiles.listRepos();
// Pick by full name
const repo = repos.find(r => r.repoFullName === 'owner/repo');
if (!repo) throw new Error('Repo not found');

Create a profile (returns a live URL)

const setup = await morph.browser.profiles.createProfile({
  name: 'Staging',
  repoId: repo.repoId
});

console.log('Open to sign in:', setup.session.debugUrl);
// User signs in, then persist the state:
await setup.save();

console.log('Profile ID:', setup.profile.id);

Use a profile in browser tasks

const result = await morph.browser.execute({
  task: "Go to the dashboard and verify it loads",
  url: "https://staging.myapp.com",
  profileId: setup.profile.id
});

Update a profile (add more logins)

const edit = await morph.browser.profiles.updateProfile(setup.profile.id);
console.log('Open to sign in:', edit.session.debugUrl);
await edit.save();

List and delete profiles

const profiles = await morph.browser.profiles.listProfiles(repo.repoId);
await morph.browser.profiles.deleteProfile(profiles[0].id);

Debug with Recordings

Enable video and logs for failed tests:
const result = await morph.browser.execute({
  task: "Complete the checkout flow",
  url: "https://shop.example.com",
  recordVideo: true,
  maxSteps: 20
});

if (!result.success && result.recordingId) {
  const rec = await morph.browser.getRecording(result.recordingId);
  console.log('📹 Video:', rec.videoUrl);
  console.log('🌐 Network:', rec.networkUrl);
  console.log('📊 Console:', rec.consoleUrl);
}

CI/CD Integration

Track tests with reference IDs:
const result = await morph.browser.execute({
  task: "Verify homepage loads correctly",
  url: process.env.PREVIEW_URL,
  externalId: process.env.GITHUB_PR_NUMBER,
  repoFullName: process.env.GITHUB_REPOSITORY, // "owner/repo"
  commitId: process.env.GITHUB_SHA,
  recordVideo: true
});

// Link results back to your PR/build system

Live Sessions

Stream browser execution in real-time at 25 fps. Perfect for debugging, monitoring, or human-in-the-loop workflows.

Basic Live Streaming

const task = await morph.browser.createTask({
  task: "Test the payment flow",
  url: "https://myapp.com"
});

// Available immediately (~2s)
console.log('Watch live:', task.debugUrl);
// Share with team, embed in dashboard, or monitor yourself

const result = await task.complete();

Embed in Dashboard

const task = await morph.browser.createTask({
  task: "Monitor production health check",
  url: "https://myapp.com"
});

// Read-only viewer
const viewer = task.getLiveIframe?.('readonly');
document.getElementById('monitor').innerHTML = viewer;

// Interactive control (human takeover)
const controller = task.getLiveIframe?.({
  interactive: true,
  height: '800px',
  width: '100%'
});
Live URLs are unauthenticated. Anyone with the URL can view (and control if interactive=true) the session. Add your own auth for production use.

Built-in Tools

The agent automatically uses tools when needed based on your task description.

Responsive Testing

Test layouts at different screen sizes—just mention dimensions in your task:
const result = await morph.browser.execute({
  task: "Resize to iPhone 12 size (390x844) and verify the mobile nav works",
  url: "https://myapp.com"
});
Common dimensions:
  • Desktop: 1920x1080, 1440x900, 1280x720
  • Tablet: 1024x768 (iPad), 768x1024 (iPad Portrait)
  • Mobile: 375x667 (iPhone SE), 414x896 (iPhone 11), 390x844 (iPhone 12/13)
More tools coming: file uploads, API interactions, human-in-the-loop. All tools are globally available.

Recordings

Enable recordVideo: true to capture full session details:
const result = await morph.browser.execute({
  task: "Test the checkout flow",
  url: "https://shop.example.com",
  recordVideo: true
});

if (result.recordingId) {
  const recording = await morph.browser.getRecording(result.recordingId);
  
  console.log('📹 Video:', recording.videoUrl);       // MP4/WebM playback
  console.log('🔄 Interactive:', recording.replayUrl); // rrweb DOM replay
  console.log('🌐 Network:', recording.networkUrl);    // All HTTP requests
  console.log('📊 Console:', recording.consoleUrl);    // JS console logs
}
What you get:
  • Video file (MP4/WebM)
  • Interactive DOM replay (rrweb)
  • Network logs (all requests/responses)
  • Console logs (JS output)
  • Screenshots (per step)

Get Animated WebP

Convert recordings to animated WebP for embedding in PRs or dashboards:
const recording = await morph.browser.getRecording(result.recordingId);

const { webpUrl } = await recording.getWebp({
  width: 780,
  fps: 10,
  quality: 65,
  maxDuration: 15
});

console.log(`![Recording](${webpUrl})`);
With file size budget:
const { webpUrl } = await recording.getWebp({
  maxSizeMb: 2.0  // Output guaranteed ≤ 2MB
});
When maxSizeMb is set, the output is guaranteed to stay under that size. For long recordings with tight budgets, the video is automatically sped up to fit. Cached in S3—subsequent calls return instantly.

Get Errors with Screenshots

const recording = await morph.browser.getRecording(result.recordingId);

const { errors, totalErrors } = await recording.getErrors();

errors.forEach(err => {
  console.log(`[${err.type}] ${err.message}`);
  if (err.screenshotUrl) {
    console.log(`Screenshot: ${err.screenshotUrl}`);
  }
});

Choosing a Model

Specify which model to use for browser automation with the model parameter:
const result = await morph.browser.execute({
  task: "Test the checkout flow",
  url: "https://myapp.com",
  model: "morph-computer-use-v1"  // or "morph-computer-use-v0", "gemini-3-flash-preview"
});

Available Models

ModelDescription
morph-computer-use-v1Default. Latest Morph model, optimized for browser automation
morph-computer-use-v0Legacy Morph model, stable fallback
gemini-3-flash-previewGoogle Gemini 3 Flash, uses external Google API
morph-computer-use-v1 is the default and recommended for most use cases. Use gemini-3-flash-preview if you prefer Google’s Gemini model (requires GOOGLE_API_KEY on the server).

API Reference

execute()

Synchronous execution—waits for completion (~30-60s).
const result = await morph.browser.execute({
  // Required
  task: string,              // Natural language description
  
  // Optional
  url?: string,              // Starting URL
  model?: string,            // See "Choosing a Model" below
  maxSteps?: number,         // 1-50 (default: 10)
  recordVideo?: boolean,     // Enable recording (default: false)
  viewportWidth?: number,    // Browser width (default: 1280)
  viewportHeight?: number,   // Browser height (default: 720)
  externalId?: string,       // Your tracking ID
  repoFullName?: string,     // Repository full name ("owner/repo")
  repoId?: string,           // Repository UUID (if you already have it)
  commitId?: string,         // Commit/version identifier
  
  // Authentication
  auth?: {                   // Global credentials
    username?: string,
    password?: string,
    cookies?: any            // Cookie array for pre-authenticated sessions
  } | Record<string, AuthCredentials>  // Per-domain credentials
});
Returns:
{
  success: boolean,          // Agent self-assessment
  result?: string,           // Task result/findings
  error?: string,            // Error message if failed
  stepsTaken: number,        // Actions executed
  executionTimeMs: number,
  
  // Debugging
  urls: (string | null)[],   // All URLs visited
  actionNames: string[],     // All actions taken
  errors: (string | null)[], // Per-step errors
  
  // Recording
  recordingId?: string,
  recordingStatus?: string,  // PENDING | RUNNING | PROCESSING | COMPLETED | ERROR
  debugUrl?: string          // Live session URL
}

createTask()

Async execution—returns immediately with live URL (~2s).
const task = await morph.browser.createTask({
  // Same parameters as execute()
  task: "Test the login flow",
  url: "https://myapp.com"
});

console.log(task.debugUrl);    // Watch live
const result = await task.complete(); // Wait for completion

Writing Good Tasks

Be specific—the agent performs better with clear instructions. ✅ Good (specific):
  • “Navigate to pricing and verify all three tiers display”
  • “Add item to cart, go to checkout, verify subtotal matches”
  • “Test login with test@example.com / password123”
❌ Bad (vague):
  • “test the app”
  • “check if everything works”
  • “make sure there are no bugs”
Choosing maxSteps:
  • 5-10 steps: Simple navigation and verification
  • 10-15 steps: Form submissions, multi-step flows
  • 15-30 steps: Complex journeys (checkout, onboarding)

Remote URLs Only

The browser runs on our infrastructure—it cannot access localhost. ✅ Use these:
  • https://3000-abc.e2b.dev (e2b tunnel)
  • https://preview-abc.vercel.app (Vercel preview)
  • https://staging.myapp.com (staging environment)
❌ Don’t use:
  • localhost:3000
  • 127.0.0.1
  • 192.168.x.x or any local IPs
Tunnel local servers with e2b.dev, ngrok, or deploy to Vercel for instant preview URLs.

Python SDK

Morph is OpenAI-compatible. Use with browser-use Python SDK:
from browser_use import Agent, ChatOpenAI
import os

llm = ChatOpenAI(
    model="morph-computer-use-v1",  # or "morph-computer-use-v0", "gemini-3-flash-preview"
    api_key="YOUR_API_KEY",
    base_url="https://api.morphllm.com/v1"
)

agent = Agent(
    task="Navigate to amazon.com and get the first product title",
    llm=llm
)

result = await agent.run(max_steps=10)
Get browser-use’s Python interface with Morph’s 10x cheaper pricing. See the full guide.

Setting Up Test Accounts

Use a separate development environment for browser automation testing. This lets you freely disable 2FA, bot protection, and other security features without affecting production.
Most auth providers (Clerk, Auth0, Supabase, Firebase) support creating separate development/staging instances. Create your test accounts there, where you can:
  • Disable 2FA/MFA on test accounts
  • Turn off bot protection and CAPTCHA
  • Skip email verification
  • Add preview URL wildcards to allowed origins (e.g., https://*.vercel.app)

Quick Setup by Provider

  1. Create a Development instance in Clerk Dashboard
  2. Create test user: Dashboard → Users → Create user
  3. Disable bot protection: Configure → Attack Protection → Bot Protection
  4. Add allowed origins: Configure → Paths → add https://*.vercel.app
  5. Use development keys in preview environment:
# Vercel Preview environment variables
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
  1. Create a Development tenant in Auth0
  2. Create test user: User Management → Users → Create User
  3. Disable MFA: Security → Multi-factor Auth → disable or add rule to skip for test emails
  4. Add allowed origins: Applications → Settings → Allowed Web Origins
  5. Use development tenant in preview:
AUTH0_ISSUER_BASE_URL=https://dev-xxx.auth0.com
AUTH0_CLIENT_ID=dev_client_id
  1. Create a separate Supabase project for staging
  2. Create test user via Dashboard or SQL
  3. Disable email confirmation: Authentication → Providers → Email
  4. Disable CAPTCHA: Authentication → Settings
  5. Add redirect URLs: Authentication → URL Configuration → add https://*.vercel.app/**
# Vercel Preview environment
NEXT_PUBLIC_SUPABASE_URL=https://xxx-staging.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=staging_anon_key
  1. Create a separate Firebase project for development
  2. Create test user and mark email as verified
  3. Add authorized domains: Authentication → Settings → Authorized domains
  4. For CI/CD, consider using Firebase Auth Emulator
FIREBASE_AUTH_EMULATOR_HOST=localhost:9099

Vercel Environment Variables

Configure different auth credentials per environment:
# Vercel Dashboard → Settings → Environment Variables

# Production: Use production auth instance
CLERK_SECRET_KEY=sk_live_xxx  (Production only)

# Preview: Use development auth instance
CLERK_SECRET_KEY=sk_test_xxx  (Preview only)
TEST_USER_EMAIL=test@yourdomain.com  (Preview only)
TEST_USER_PASSWORD=xxx  (Preview only)
Then use with Morph:
const result = await morph.browser.execute({
  task: "Log in with x_user and x_pass, verify dashboard",
  url: "https://preview-abc.vercel.app/login",
  auth: {
    username: process.env.TEST_USER_EMAIL,
    password: process.env.TEST_USER_PASSWORD
  }
});

FAQ

The browser runs on our servers, not your machine.Won’t work: localhost:3000, 127.0.0.1, 192.168.x.xSolutions:
  • Deploy to Vercel/Netlify for instant preview URLs
  • Tunnel with e2b.dev or ngrok
  • Use a staging environment
execute() - Waits for completion (~30-60s)
const result = await morph.browser.execute({
  task: "Test login",
  url: "https://app.com"
});
// Returns complete result
createTask() - Returns immediately (~2s)
const task = await morph.browser.createTask({
  task: "Test login",
  url: "https://app.com"
});
console.log(task.debugUrl); // Watch live!
const result = await task.complete();
Use createTask() to watch from the start or get the live URL for monitoring/debugging.
Every task returns rich debugging data automatically:
const result = await morph.browser.execute({
  task: "Test checkout",
  url: "https://myapp.com",
  recordVideo: true,
  maxSteps: 20
});

if (!result.success) {
  // Always available
  console.log('Error:', result.error);
  console.log('URLs:', result.urls);
  console.log('Actions:', result.actionNames);
  console.log('Step errors:', result.errors.filter(e => e));
  
  // With recording
  if (result.recordingId) {
    const rec = await morph.browser.getRecording(result.recordingId);
    console.log('Video:', rec.videoUrl);
    console.log('Network:', rec.networkUrl);
  }
}
You get: agent self-assessment, all URLs visited, every action taken, per-step errors, and optional video/logs.
Each browser action = 1 step:
  • Click → 1 step
  • Fill form → 1 step
  • Navigate → 1 step
  • Wait → 1 step
Sizing:
  • 5-10 steps: Simple tasks
  • 10-15 steps: Forms, multi-step
  • 15-30 steps: Complex flows
Hit the limit? Increase maxSteps or simplify the task.
Yes—use Zod schemas with createTask():
import { z } from 'zod';

const task = await morph.browser.createTask({
  task: "Get the product price",
  url: "https://store.com/product",
  schema: z.object({
    price: z.number(),
    inStock: z.boolean()
  })
});

const result = await task.complete();
console.log(result.parsed); // { price: 29.99, inStock: true }
Enable recordVideo: true to get:
  • Video (MP4/WebM) - Visual playback
  • rrweb replay - Interactive DOM timeline
  • Network logs - All HTTP requests
  • Console logs - JS console output
  • Screenshots - Per step
Stored in S3 with 7-day presigned URLs.
WebRTC streaming at 25 fps. Use cases:Monitoring:
const iframe = task.getLiveIframe?.('readonly');
// Embed in dashboard
Human takeover:
const control = task.getLiveUrl?.({ interactive: true });
// Send to operator
Debugging:
console.log(task.debugUrl);
// Share with team
Compatibility: Chrome 90+, Firefox 90+, Safari 14.1+
Yes—works with any OpenAI SDK:
import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: "YOUR_API_KEY",
  baseURL: 'https://api.morphllm.com/v1'
});

const completion = await client.chat.completions.create({
  model: 'morph-computer-use-v1',
  messages: [{ role: 'user', content: 'Test checkout' }]
});
Works with: browser-use, Browserbase, Browserless, Steel, and more.
Link tests to your systems with reference IDs:
const result = await morph.browser.execute({
  task: "Test checkout",
  url: "https://myapp.com",
  externalId: "PR-1234",    // PR, Jira, CI/CD
  repoFullName: "myorg/myrepo", // Repository
  commitId: "abc123def456"  // Specific commit
});
All fields optional. Filter by these IDs in the dashboard.
Use the auth parameter with username/password or cookies:
// Username/password login
const result = await morph.browser.execute({
  task: "Log in with x_user and x_pass, then verify dashboard",
  url: "https://myapp.com/login",
  auth: { username: "test@example.com", password: "secret" }
});

// Skip login with cookies
const result = await morph.browser.execute({
  task: "Verify admin dashboard",
  url: "https://myapp.com/admin",
  auth: {
    cookies: [{ name: "session", value: "abc123", domain: "myapp.com" }]
  }
});
Reference credentials in your task as x_user and x_pass.
Third-party auth providers like Clerk, Auth0, and Supabase Auth often block requests from unfamiliar origins or automated browsers.Solutions:
  1. Use cookie-based auth (recommended):
    const result = await morph.browser.execute({
      task: "Verify dashboard loads",
      url: "https://myapp.com/dashboard",
      auth: {
        cookies: [
          { name: "__session", value: "your-session-cookie", domain: "myapp.com" }
        ]
      }
    });
    
    Export cookies from your browser’s DevTools after logging in manually.
  2. Add preview URL to your auth provider’s allowed origins:
    • Clerk: Dashboard → API Keys → Allowed origins → Add your preview URL
    • Auth0: Applications → Settings → Allowed Origins → Add preview URL
    • Supabase: Authentication → URL Configuration → Add preview URL
    Example: Add https://your-preview-abc.vercel.app to allowed origins.
Cookie auth is more reliable since it bypasses the login flow entirely.

Need Help?