Overview
Every Dual Login profile is a real browser with its own fingerprint, cookies and proxy. There are two ways to control one — pick whichever fits:
How it works
Local vs Global
The action names are identical in both modes — only the base URL and how you target the profile differ.
# base = http://127.0.0.1:4482 — profile in the PATH
curl http://127.0.0.1:4482/api/profiles
curl -X POST http://127.0.0.1:4482/api/profiles/PROFILE_ID/goto \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}'# 1) log in -> token
curl -X POST https://api.duallogin.com/globally/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"****"}'
# => {"token":"eyJ..."}
# 2) call actions — profile in the BODY + bearer token
curl -X POST https://api.duallogin.com/globally/goto \
-H 'Authorization: Bearer eyJ...' -H 'Content-Type: application/json' \
-d '{"profile":"PROFILE_ID","url":"https://example.com"}'Profiles & status
Read-only lookups — list profiles, see what's running, grab a profile's CDP endpoint.
| Method | Endpoint (local) | Returns |
|---|---|---|
| GET | /api/profiles | All profiles (id, name, fingerprint, proxy) |
| GET | /api/running | Profiles currently open |
| GET | /api/profiles/{id} | One profile |
| GET | /api/profiles/{id}/tabs | Open tabs (number · title · URL) |
| GET | /api/profiles/{id}/cookies | Live cookies |
| GET | /api/profiles/{id}/cdp | CDP port + URLs for Selenium/Puppeteer/Playwright |
Profile control
Drive a launched profile. Local: POST /api/profiles/{id}/<action>. Global: POST /globally/<action> with {profile} in the body.
| Action | Body | What it does |
|---|---|---|
| launch | {} | Open the profile's browser |
| stop | {} | Close it |
| goto | {tab,url} | Navigate |
| click | {tab,selector|text|xpath|x,y} | Real mouse click (auto-searches iframes) |
| type | {tab,selector,text,delay} | Type into a field |
| set-value | {tab,selector,value} | Clear & set a field value |
| press | {tab,key} | Press a key / chord (Enter, Tab…) |
| hover · scroll · select | {tab,selector,…} | Hover / scroll-to / pick a <select> option |
| wait | {tab,selector,timeout} or {ms} | Wait for an element / sleep |
| get | {tab,selector,attribute} | Read text / value / html / attribute |
| eval | {tab,expression} | Run JS, get the result |
| screenshot | {tab,fullPage} | Screenshot (base64 + data URL) |
| new-tab · close-tab | {url} / {tab} | Open / close a tab |
| reload · back · forward | {tab} | History controls |
Update fingerprint
Create a profile with a fresh identity, tweak its fingerprint, or generate one to preview.
| Method | Endpoint | Body / query |
|---|---|---|
| POST | /api/profiles | {name, os, country, proxy, startUrl} → new profile + auto fingerprint |
| PATCH | /api/profiles/{id} | {fingerprint:{os,timezone,languages,...}} → update fields |
| GET | /api/fingerprint/generate | ?os=Windows&country=US → a ready-to-use fingerprint |
# new profile with a US Windows identity behind a proxy
curl -X POST http://127.0.0.1:4482/api/profiles -H 'Content-Type: application/json' -d '{
"name":"US-01","os":"Windows","country":"US",
"proxy":{"server":"http://host:port","username":"u","password":"p"}
}'
# change its timezone + language
curl -X PATCH http://127.0.0.1:4482/api/profiles/PROFILE_ID -H 'Content-Type: application/json' \
-d '{"fingerprint":{"timezone":"America/New_York","languages":["en-US","en"]}}'Sample · curl / HTTP
No driver needed — just HTTP. Launch a profile, open a page, wait, and read the heading. Replace PROFILE_ID.
import requests
BASE = "http://127.0.0.1:4482"
PID = "PROFILE_ID"
def act(name, **body):
r = requests.post(f"{BASE}/api/profiles/{PID}/{name}", json=body)
r.raise_for_status()
return r.json()
requests.post(f"{BASE}/api/profiles/{PID}/launch", json={})
act("goto", tab=1, url="https://example.com")
act("wait", tab=1, selector="h1", timeout=8000)
print("Heading:", act("get", tab=1, selector="h1"))const BASE = "http://127.0.0.1:4482";
const PID = "PROFILE_ID";
const act = (name, body = {}) =>
fetch(`${BASE}/api/profiles/${PID}/${name}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then((r) => r.json());
await act("launch");
await act("goto", { tab: 1, url: "https://example.com" });
await act("wait", { tab: 1, selector: "h1", timeout: 8000 });
console.log("Heading:", await act("get", { tab: 1, selector: "h1" }));using System.Net.Http;
using System.Text;
var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:4482") };
const string PID = "PROFILE_ID";
async Task<string> Act(string name, string json = "{}") {
var res = await http.PostAsync($"/api/profiles/{PID}/{name}",
new StringContent(json, Encoding.UTF8, "application/json"));
return await res.Content.ReadAsStringAsync();
}
await Act("launch");
await Act("goto", "{\"tab\":1,\"url\":\"https://example.com\"}");
await Act("wait", "{\"tab\":1,\"selector\":\"h1\",\"timeout\":8000}");
Console.WriteLine(await Act("get", "{\"tab\":1,\"selector\":\"h1\"}"));package main
import ("bytes"; "fmt"; "io"; "net/http")
const base, pid = "http://127.0.0.1:4482", "PROFILE_ID"
func act(name, body string) string {
url := fmt.Sprintf("%s/api/profiles/%s/%s", base, pid, name)
r, _ := http.Post(url, "application/json", bytes.NewBufferString(body))
defer r.Body.Close()
b, _ := io.ReadAll(r.Body)
return string(b)
}
func main() {
act("launch", "{}")
act("goto", `{"tab":1,"url":"https://example.com"}`)
act("wait", `{"tab":1,"selector":"h1","timeout":8000}`)
fmt.Println(act("get", `{"tab":1,"selector":"h1"}`))
}<?php
$base = "http://127.0.0.1:4482"; $pid = "PROFILE_ID";
function act($name, $body = []) {
global $base, $pid;
$ch = curl_init("$base/api/profiles/$pid/$name");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode((object)$body),
CURLOPT_RETURNTRANSFER => true,
]);
return curl_exec($ch);
}
act("launch");
act("goto", ["tab" => 1, "url" => "https://example.com"]);
act("wait", ["tab" => 1, "selector" => "h1", "timeout" => 8000]);
echo act("get", ["tab" => 1, "selector" => "h1"]);Sample · Puppeteer
Attach Puppeteer to a running profile over its CDP endpoint (from GET /api/profiles/{id}/cdp).
import puppeteer from "puppeteer-core";
const PID = "PROFILE_ID";
// make sure it's running: POST /api/profiles/PID/launch
const cdp = await fetch(`http://127.0.0.1:4482/api/profiles/${PID}/cdp`).then(r => r.json());
// cdp = { port, httpEndpoint, puppeteer, playwright, selenium }
const browser = await puppeteer.connect({ browserURL: cdp.httpEndpoint });
const page = (await browser.pages())[0] ?? await browser.newPage();
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
console.log("Title:", await page.title());
await browser.disconnect(); // leaves the profile openSample · Playwright
Connect over CDP with connectOverCDP — Node or Python.
import { chromium } from "playwright";
const PID = "PROFILE_ID";
const cdp = await fetch(`http://127.0.0.1:4482/api/profiles/${PID}/cdp`).then(r => r.json());
const browser = await chromium.connectOverCDP(cdp.httpEndpoint);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] ?? await ctx.newPage();
await page.goto("https://example.com");
console.log("Title:", await page.title());
await browser.close();import requests
from playwright.sync_api import sync_playwright
PID = "PROFILE_ID"
cdp = requests.get(f"http://127.0.0.1:4482/api/profiles/{PID}/cdp").json()
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp["httpEndpoint"])
ctx = browser.contexts[0]
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://example.com")
print("Title:", page.title())
browser.close()Sample · Selenium
Attach Selenium to the running profile via its debugger address (the port from /cdp).
import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
PID = "PROFILE_ID"
cdp = requests.get(f"http://127.0.0.1:4482/api/profiles/{PID}/cdp").json()
opts = Options()
opts.add_experimental_option("debuggerAddress", f"127.0.0.1:{cdp['port']}")
driver = webdriver.Chrome(options=opts) # attaches to the live profile
driver.get("https://example.com")
print("Title:", driver.title)// GET http://127.0.0.1:4482/api/profiles/PROFILE_ID/cdp -> { "port": 51234 }
ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("debuggerAddress", "127.0.0.1:51234");
WebDriver driver = new ChromeDriver(options);
driver.get("https://example.com");
System.out.println("Title: " + driver.getTitle());// GET http://127.0.0.1:4482/api/profiles/PROFILE_ID/cdp -> { "port": 51234 }
var options = new OpenQA.Selenium.Chrome.ChromeOptions();
options.DebuggerAddress = "127.0.0.1:51234";
var driver = new OpenQA.Selenium.Chrome.ChromeDriver(options);
driver.Navigate().GoToUrl("https://example.com");
System.Console.WriteLine("Title: " + driver.Title);Sample · Raw CDP websocket
Lowest level — open the DevTools websocket and send CDP commands yourself. Get the browser ws URL from http://127.0.0.1:{port}/json/version.
import WebSocket from "ws";
const PID = "PROFILE_ID";
const { port } = await fetch(`http://127.0.0.1:4482/api/profiles/${PID}/cdp`).then(r => r.json());
const { webSocketDebuggerUrl } = await fetch(`http://127.0.0.1:${port}/json/version`).then(r => r.json());
const ws = new WebSocket(webSocketDebuggerUrl);
let id = 0;
const send = (method, params = {}) => ws.send(JSON.stringify({ id: ++id, method, params }));
ws.on("open", () => send("Target.createTarget", { url: "https://example.com" }));
ws.on("message", (m) => console.log(m.toString()));# pip install websocket-client requests
import json, requests, websocket
PID = "PROFILE_ID"
port = requests.get(f"http://127.0.0.1:4482/api/profiles/{PID}/cdp").json()["port"]
ws_url = requests.get(f"http://127.0.0.1:{port}/json/version").json()["webSocketDebuggerUrl"]
ws = websocket.create_connection(ws_url)
ws.send(json.dumps({"id": 1, "method": "Target.createTarget",
"params": {"url": "https://example.com"}}))
print(ws.recv())
ws.close()// go get github.com/gorilla/websocket
package main
import ("encoding/json"; "fmt"; "net/http"; "github.com/gorilla/websocket")
func main() {
var cdp struct{ Port int `json:"port"` }
r, _ := http.Get("http://127.0.0.1:4482/api/profiles/PROFILE_ID/cdp")
json.NewDecoder(r.Body).Decode(&cdp)
var ver struct{ WS string `json:"webSocketDebuggerUrl"` }
r2, _ := http.Get(fmt.Sprintf("http://127.0.0.1:%d/json/version", cdp.Port))
json.NewDecoder(r2.Body).Decode(&ver)
c, _, _ := websocket.DefaultDialer.Dial(ver.WS, nil)
defer c.Close()
c.WriteJSON(map[string]any{"id": 1, "method": "Target.createTarget",
"params": map[string]string{"url": "https://example.com"}})
_, msg, _ := c.ReadMessage()
fmt.Println(string(msg))
}