Documentation
Driving Ward from a script
Ward answers a small HTTP API on this computer. A script asks it to open a profile and gets back the browser’s DevTools address, with the proxy relay running and the device identity already applied. Nothing about it leaves the machine.
What you get back
An operator with forty profiles does not want to press Start forty times. Every product in this category ships an API for that reason, and this is Ward’s.
The part worth understanding is what you get back. Starting a profile returns a browserWSEndpoint, which is exactly what Puppeteer’s connect({browserWSEndpoint}) and Selenium’s debuggerAddress take. By the time you have it, Ward has probed the proxy, started the loopback relay that holds its credentials, aligned the timezone to the exit country and applied the profile’s device identity over the debugging protocol.
A script that launches Chrome itself gets a browser with none of that: no relay, no identity, no WebRTC pinning, browsing from your own address. Attach to the one Ward hands you.
Get a token
Open Settings and find Scripts and agents. Press New token, name it after whatever is going to use it, and choose what it may do. The token is shown once - Ward stores only a fingerprint of it, so a token you did not write down is one to revoke and replace.
The same screen tells you the address to point at. It looks like this:
$ward = "http://127.0.0.1:7817"
$token = "the token you just copied"
Invoke-RestMethod "$ward/v1/status" -Headers @{ Authorization = "Bearer $token" }If that answers, everything else on this page will work. If it does not, the Components screen in Ward says why - the usual reason is another program holding the port, and it has a button that moves Ward to a free one.
What a token may do
A token says what it may do. There are three, and they nest.
- Read only. List profiles, folders, proxies and device identities. Cannot open a browser. That last part matters more than it sounds: opening a profile makes real requests, from a real proxy, to accounts you are signed in to.
- Read and launch. The above, plus starting and stopping profiles. Cannot create, change or delete anything. This is what most automation actually wants.
- Full control. Everything the API exposes: creating profiles, attaching proxies, rewriting device identities, deleting rows.
Two things no scope reaches
A proxy’s username and password. You can create a proxy with credentials and point profiles at it by id. You cannot read one back, at any scope, on any route. The relay holds what the relay needs.
Minting another token. There is no endpoint for it. A client that could mint a token could mint itself a wider one, and the scopes would be a suggestion rather than a boundary. Tokens come from the window.
Open a profile and drive it
The shortest useful thing: list what you have, open one, and attach Puppeteer to the browser that comes back.
import puppeteer from 'puppeteer-core'
const ward = 'http://127.0.0.1:7817'
const headers = { Authorization: `Bearer ${process.env.WARD_TOKEN}`,
'Content-Type': 'application/json' }
const { result } = await (await fetch(`${ward}/v1/profiles`, { headers })).json()
const profile = result.profiles.find((p) => p.name === 'Acme 01')
const started = await (await fetch(`${ward}/v1/profiles/${profile.id}/start`,
{ method: 'POST', headers })).json()
if (!started.ok) throw new Error(`${started.code}: ${started.detail}`)
const browser = await puppeteer.connect({
browserWSEndpoint: started.result.browserWSEndpoint,
})
const page = await browser.newPage()
await page.goto('https://example.com')
console.log(await page.evaluate(() => navigator.userAgent))
// Detach, do not close: closing from Puppeteer takes the browser down behind Ward's back.
await browser.disconnect()
await fetch(`${ward}/v1/profiles/${profile.id}/stop`, { method: 'POST', headers })Starting takes seconds, and the wait is deliberate: Ward probes the proxy before it starts anything and refuses the launch rather than opening a window that would browse from your own address. A 409 here is a real state - the proxy is down, the browser is not installed, the profile is already open - and retrying will not change it. Read detail.
Build a fleet
A token with full control can build the fleet as well as run it. Folders, proxies and profiles, in that order:
$h = @{ Authorization = "Bearer $token"; 'Content-Type' = 'application/json' }
$folder = (Invoke-RestMethod "$ward/v1/folders" -Method Post -Headers $h `
-Body '{"name":"Acme"}').result.folder
$proxy = (Invoke-RestMethod "$ward/v1/proxies" -Method Post -Headers $h `
-Body '{"host":"gw.provider.net","port":8000,"kind":"http",
"username":"acme-01","password":"..."}').result.proxy
1..10 | ForEach-Object {
$body = @{ name = "Acme {0:D2}" -f $_; engine = "chromium"
folderId = $folder.id; proxyId = $proxy.id } | ConvertTo-Json
Invoke-RestMethod "$ward/v1/profiles" -Method Post -Headers $h -Body $body
}Each profile gets its own generated device. That is the default and it is the right one: a profile with no device identity launches a browser presenting your actual computer. If you genuinely want that for one profile, ask for it by name with "fingerprint": "none".
Do not share one identity across a fleet
Check GET /v1/engines before you start. Creating ten profiles on an engine this machine does not have produces ten profiles that refuse to launch, and the refusal arrives ten times.
Responses and refusals
Every response is one of two shapes.
{ "ok": true, "result": { ... } }
{ "ok": false, "code": "ProxyUnreachable", "detail": "..." }- 400 - the request was wrong.
detailsays how. - 401 - no token, or one this host will not accept. A revoked token and a token that never existed get the same sentence, on purpose.
- 403 - the token is fine and its scope does not reach that route. It will not change on retry.
- 404 - no such id, or the profile was not running.
- 409 - well formed, and cannot be done in this state.
What the API is and is not exposed to
The listener binds to 127.0.0.1 and nothing else. It refuses a Host header naming anything but loopback on its own port, which is what stops a web page you happen to visit from reaching it through a rebound DNS name. A page cannot send an Authorization header on a cross-origin request without a preflight, and Ward answers a preflight only for an origin you have allowlisted - which by default is none.
What that leaves is other processes running as you. Any of them can read the token file, and any of them could have read your browser profiles directly anyway. Ward does not claim otherwise, and the scopes exist so that the token you hand to something is the smallest one that does the job.
Deleting a profile keeps its data
DELETE /v1/profiles/{id} removes the database row and leaves the profile directory exactly where it is, with every cookie and signed-in session in it. The response tells you the path. Nothing in Ward deletes one, on any route, ever.The full route list
The full list of routes is served by your own copy of Ward, generated from the route table it actually dispatches on - so it describes the build in front of you rather than the version this page was last edited for.
curl -H "Authorization: Bearer $WARD_TOKEN" http://127.0.0.1:7817/v1/openapi.json
curl -H "Authorization: Bearer $WARD_TOKEN" http://127.0.0.1:7817/v1/agent-guideThe second one is markdown written for a language model. If you are pointing Claude or Codex at Ward, the guide for that is here.