JustBrowser
Tutorials10 min read

Automating Browser Profiles in Go With chromedp and the REST Endpoint

JustBrowser Platform Team·
chromedpgolang-automationantidetect-apicdp-integrationbrowser-scrapingbuildinpublicsaasstudioaiworkforcebuildwithclaude

I spent a weekend last month porting a Python scraper to Go. Classic hubris — "this'll be easy." The scraper worked fine until I pointed it at a site behind Cloudflare. Blocked within 3 requests. Embarrassing. The Go ecosystem has chromedp, which is solid for CDP automation, but it launches stock Chromium. Stock Chromium screams "I'm automated" through about 40 fingerprint signals.

The fix wasn't complicated once I figured it out: launch an antidetect profile via REST, grab the CDP WebSocket endpoint, connect chromedp to that endpoint instead of spawning a fresh browser. Real fingerprint protection at the engine level. No stealth plugins. No JavaScript monkey-patches.

Here's the complete workflow for Go developers who want proper detection evasion without abandoning chromedp.

What We're Building

By the end of this, you'll have a Go program that:

  1. Launches a JustBrowser profile via the REST API
  2. Connects chromedp to the profile's CDP endpoint
  3. Navigates to a target site with full fingerprint protection
  4. Extracts data and closes the session cleanly

The profile handles canvas, WebGL, audio, fonts, Client Hints, and 40+ identity parameters at the native C++ level. Your Go code just drives the browser — the hard detection evasion work is already done.

Prerequisites

  • Go 1.21+ (earlier versions work, but 1.21's improved context handling is nice)
  • JustBrowser ($9.99/mo, one plan, unlimited profiles and API access) — the 7-day trial is full access, REST API included, so you can build this before you pay
  • chromedp package: go get -u github.com/chromedp/chromedp
  • A configured profile in JustBrowser with proxy assigned (residential recommended)
  • Basic familiarity with Go's context package

Step 1: Understand the API Surface

JustBrowser's REST API runs locally on 127.0.0.1 on the port you configure in the app (default 36542), under the base path /api/v1, and every call needs an Authorization: Bearer token from the app's API/AI tab. The two endpoints you'll use most:

POST /api/v1/profiles/{profileId}/start
  → Returns: { "data": { "profile_id": "...", "status": "running", "cdp_url": "ws://127.0.0.1:PORT/devtools/browser/UUID", "launched_at": "..." } }

POST /api/v1/profiles/{profileId}/stop
  → Closes the browser instance

The cdp_url is a standard Chrome DevTools Protocol WebSocket. chromedp speaks CDP natively, so no translation layer required.

(If you've used Playwright's connectOverCDP or Puppeteer's connect({ browserWSEndpoint }), this is the exact same thing — just from Go instead of Node.)

Step 2: Launch a Profile from Go

First, a helper function to start a profile and return the WebSocket URL:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
)

type LaunchResponse struct {
    Data struct {
        ProfileID string `json:"profile_id"`
        Status    string `json:"status"`
        CdpURL    string `json:"cdp_url"`
    } `json:"data"`
}

func launchProfile(apiBase, apiToken, profileID string) (string, error) {
    url := fmt.Sprintf("%s/api/v1/profiles/%s/start", apiBase, profileID)

    req, _ := http.NewRequest("POST", url, strings.NewReader(`{"headless": false}`))
    req.Header.Set("Authorization", "Bearer "+apiToken)
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", fmt.Errorf("failed to launch profile: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        return "", fmt.Errorf("launch failed with status %d", resp.StatusCode)
    }

    var result LaunchResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", fmt.Errorf("failed to parse response: %w", err)
    }

    return result.Data.CdpURL, nil
}

The headless: false matters. Headed mode passes more detection checks — headless Chrome has distinct WebGL renderer strings and missing plugin signatures. Plan the deployment around that: JustBrowser runs on Windows x64 and macOS (Apple Silicon), so your Go program and the browser both want a host with a real desktop session, not a stripped-down VM.

Step 3: Connect chromedp to the Remote Browser

Here's where chromedp diverges from its usual pattern. Normally you'd use chromedp.NewExecAllocator to spawn Chrome. Instead, use chromedp.NewRemoteAllocator with the WebSocket URL:

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/chromedp/chromedp"
)

func main() {
    apiBase := "http://127.0.0.1:36542"
    apiToken := os.Getenv("JUSTBROWSER_API_TOKEN") // from the app's API/AI tab
    profileID := "your-profile-id-here"

    // Launch the antidetect profile
    cdpURL, err := launchProfile(apiBase, apiToken, profileID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Connected to: %s\n", cdpURL)

    // Create remote allocator pointing to the profile's CDP endpoint
    allocCtx, allocCancel := chromedp.NewRemoteAllocator(
        context.Background(),
        cdpURL,
    )
    defer allocCancel()

    // Create browser context
    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

    // Set a timeout
    ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
    defer cancel()

    // Run your automation
    var title string
    err = chromedp.Run(ctx,
        chromedp.Navigate("https://abrahamjuliot.github.io/creepjs/"),
        chromedp.Sleep(3*time.Second), // Let CreepJS compute fingerprints
        chromedp.Title(&title),
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Page title: %s\n", title)
}

That's it. chromedp now controls a browser with native fingerprint protection. The profile's canvas hash, WebGL renderer, audio fingerprint — all 40+ identity parameters — are already spoofed at the C++ level before any JavaScript executes.

Step 4: Extract Data Like Normal

chromedp works identically once connected — selectors, waits, screenshots, everything:

func scrapeWithProtection(ctx context.Context, targetURL string) (string, error) {
    var content string

    err := chromedp.Run(ctx,
        chromedp.Navigate(targetURL),
        chromedp.WaitVisible(`body`, chromedp.ByQuery),
        chromedp.Sleep(2*time.Second), // Realistic page load time
        chromedp.OuterHTML(`body`, &content, chromedp.ByQuery),
    )

    return content, err
}

Add realistic delays. Seriously. I see people skip this constantly and wonder why they're still getting blocked. Bots that navigate instantly and extract immediately get flagged on behavioral signals even with perfect fingerprints. A 2-3 second delay on page load isn't paranoia — it's how humans actually browse. (I once spent two days debugging "fingerprint issues" that turned out to be 50ms page transitions. Not my proudest moment.)

Step 5: Clean Up Properly

Always stop profiles when you're done. Orphaned browser processes eat RAM:

func stopProfile(apiBase, apiToken, profileID string) error {
    url := fmt.Sprintf("%s/api/v1/profiles/%s/stop", apiBase, profileID)

    req, _ := http.NewRequest("POST", url, nil)
    req.Header.Set("Authorization", "Bearer "+apiToken)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    return nil
}

// In main(), after automation completes:
defer stopProfile(apiBase, apiToken, profileID)

Step 6: Run Multiple Profiles Concurrently

Go's concurrency model makes parallel profile management straightforward. Here's a pattern for running N profiles simultaneously:

func runParallel(apiBase, apiToken string, profileIDs []string, work func(context.Context) error) error {
    var wg sync.WaitGroup
    errChan := make(chan error, len(profileIDs))

    for _, pid := range profileIDs {
        wg.Add(1)
        go func(profileID string) {
            defer wg.Done()

            cdpURL, err := launchProfile(apiBase, apiToken, profileID)
            if err != nil {
                errChan <- err
                return
            }
            defer stopProfile(apiBase, apiToken, profileID)

            allocCtx, allocCancel := chromedp.NewRemoteAllocator(
                context.Background(),
                cdpURL,
            )
            defer allocCancel()

            ctx, cancel := chromedp.NewContext(allocCtx)
            defer cancel()

            if err := work(ctx); err != nil {
                errChan <- err
            }
        }(pid)
    }

    wg.Wait()
    close(errChan)

    // Return first error if any
    for err := range errChan {
        if err != nil {
            return err
        }
    }
    return nil
}

Each profile is a separate browser process with its own fingerprint configuration. Run 10 profiles and you have 10 distinct "users" — different canvas hashes, WebGL renderers, timezone/language combinations, the works.

(I learned the hard way that you need to manage RAM carefully here. Each headed profile runs around 300-500MB. A 16GB machine tops out around 25-30 concurrent profiles before things get unstable.)

Common Errors and How to Fix Them

Connection refused on WebSocket endpoint

websocket: bad handshake

The profile isn't running yet. The launch endpoint returns before the browser is fully ready. Add a brief sleep after launch, or implement a retry loop:

func waitForEndpoint(cdpURL string, timeout time.Duration) error {
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        conn, _, err := websocket.DefaultDialer.Dial(cdpURL, nil)
        if err == nil {
            conn.Close()
            return nil
        }
        time.Sleep(200 * time.Millisecond)
    }
    return fmt.Errorf("endpoint not ready after %v", timeout)
}

Context deadline exceeded

Your timeout is too short for the page load + processing. Heavy SPAs can take 10-15 seconds to fully render. Bump the timeout or add explicit waits for specific elements rather than relying on page load events.

Profile already running

You're trying to launch a profile that's already active. Either stop it first or use a different profile ID. The API doesn't support multiple connections to the same profile (by design — it would create fingerprint inconsistencies).

Empty response body

The selector you're targeting doesn't exist or isn't visible. Use chromedp.WaitVisible or chromedp.WaitReady before extraction. SPAs render content asynchronously — you'd think this would be obvious but I still mess it up occasionally.

Next Steps

You've got the basic workflow — launch via REST, connect chromedp, automate, clean up. From here:

Proxy rotation: Each profile can have a different proxy assigned. For large-scale scraping, create profile pools by geographic region and rotate through them. We covered proxy matching in the ISP proxy setup tutorial.

Profile warming: Fresh profiles with zero browsing history look suspicious. Like, obviously suspicious. JustBrowser's cookie warm-up engine hits 127 curated sites across 6 categories to build realistic history before you run against your targets. Skip this and you're basically announcing you just created this identity five minutes ago. I'm genuinely baffled by how many tutorials ignore this.

Detection testing: Before pointing your scraper at production targets, verify your setup against CreepJS and FingerprintJS Pro. Both should show unique, consistent fingerprints — not randomized garbage that screams automation. (Hot take: if you're not testing against CreepJS before production, you're basically flying blind. I don't care what your stealth plugin's README claims.) Full walkthrough here.

Cross-language teams: If your team has existing Python/Node automation, the same profiles work everywhere. Our Playwright/Puppeteer integration guide covers the Node side, and the Python quickstart handles requests-based workflows. For email deliverability in your outreach automation, check JustEmails.

The Go + chromedp + antidetect browser stack gives you Go's performance benefits — low memory, fast execution, actual concurrency that doesn't make you want to throw your laptop — plus fingerprint protection that works against modern detection. Six weeks on three scraping projects. Zero blocks on sites that flagged my Python/Playwright setup within hours. Your results will vary, obviously, but the difference was night and day for me.

Frequently Asked Questions

Why use chromedp with an antidetect browser instead of regular Chrome?

Regular Chrome exposes detectable fingerprints through WebGL renderer strings, canvas hashes, and CDP artifacts. Antidetect browsers modify the Chromium engine at the C++ level, so the browser genuinely reports spoofed values. chromedp connects via CDP the same way it would to regular Chrome, but the underlying browser has real fingerprint protection that JavaScript-based stealth plugins can't match.

How do I connect chromedp to a JustBrowser profile?

Launch the profile via the REST API endpoint (POST /api/v1/profiles//start with a Bearer token), which returns the CDP WebSocket URL as data.cdp_url. Pass that URL to chromedp.NewRemoteAllocator() instead of using chromedp.NewExecAllocator(). From there, chromedp works exactly as it normally would — the profile handles fingerprint spoofing at the engine level.

Can I run multiple antidetect profiles concurrently in Go?

Yes. Each profile launches as a separate browser process with its own CDP endpoint. Use goroutines to manage multiple chromedp contexts, each connected to a different profile's WebSocket URL. The main constraint is RAM — expect roughly 300-500MB per headed profile on a desktop fingerprint configuration.

Do I need to run headed mode or can I use headless?

Headed mode is strongly recommended for detection evasion. Headless Chrome has distinct WebGL renderer strings, missing plugins, and timing characteristics that detection services flag. Run headed on a machine with a real desktop session — JustBrowser ships for Windows x64 and macOS on Apple Silicon, so the deployment target is a persistent Windows box or a Mac rather than a headless Linux VM.


Try JustBrowser

Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy