Guard.ch API reference
Every route you can call with an API key, with its fields, examples and errors.
This page lists what the API accepts and returns. For a walkthrough from API key to a running Playwright script, see the custom integration guide. To connect an AI assistant, use MCP instead.
Basics
- Base URL
- https://api.guard.ch/v7
- Authentication
- An API key in the Authorization header
- Format
- JSON over HTTPS
- Access
- Enterprise
Paths in this reference are relative to the base URL. Send request bodies as JSON with Content-Type: application/json. Responses can contain fields not listed here; ignore any you don't use.
Create API keys on the Agents tab of your dashboard, under Advanced. A key acts for your account: the sessions it starts are yours, count towards your limits and appear in your session list.
Authentication
Send the key with every request:
Authorization: Bearer YOUR_API_KEYKeys start with brl_k_ (keys created earlier with brl-k-v7-). The API reads them only from this header, never from the URL. GET /image is the only route on this page that works without a key. A key works until you revoke it, or until the fixed date chosen when it was created.
Responses and errors
Every route answers with HTTP 200 and a JSON body. The body's status field tells you the outcome, so check it instead of the HTTP code:
| Status | Meaning |
|---|---|
ok | The request succeeded. The other fields hold the result. |
denied | The request was refused, for example because the key is missing or expired, a value is invalid, no session slot is free or the session doesn't exist. |
upgrade | Your plan doesn't include this: API access itself, or an app or option you asked for. |
error | Something went wrong on our side, or the path doesn't exist. Try again later. |
Treat any status other than ok as a failure. message gives the reason in a sentence you can show to a person. Its language follows the Accept-Language header and defaults to English, so don't parse it: branch on status and, where present, code or reason.
{
"status": "denied",
"message": "This browser session could not be found. Check the session ID and try again."
}If the key is missing, invalid, revoked or expired, the answer is denied with loggedIn: false. If your email address isn't confirmed yet, it's denied with verified: false. File downloads are the only exception to HTTP 200; see Download a file.
Endpoints
| Endpoint | Description |
|---|---|
POST /web/sessions | Start a session |
GET /web/sessions | List your sessions |
GET /web/sessions/:id | Read one session |
DELETE /web/sessions/:id | Stop a session |
GET /web/sessions/:id/downloads/:downloadId | Fetch a file the browser downloaded |
GET /extensions | The apps a new or running session can use |
POST /vm/:vmId/extensions | Switch apps on in a running session |
PATCH /vm/:vmId/extensions/:id | Change an active app's settings |
DELETE /vm/:vmId/extensions/:id | Switch an app off |
GET /web/usage | Your API usage over the last 30 days |
GET /user/quota | Your plan's limits and what is in use |
GET /image | Browser images and locations |
Sessions
A session is one cloud browser. Your code drives it over CDP, and a person can watch or take over in the live view. These routes only reach sessions of your own account, never a colleague's.
Start a session
POST https://api.guard.ch/v7/web/sessionsStarts a browser and answers immediately while the browser keeps starting in the background: its status is starting, then running. Connect Playwright's connectOverCDP or Puppeteer's browserWSEndpoint to connectUrl. If the connection is refused at first, wait until GET /web/sessions/:id reports running.
All body fields are optional:
| Field | Description |
|---|---|
image | Browser image ID. Any enabled image with supports_programmatic in GET /image. Default: chrome. |
name | Display name for your session list, 1 to 64 characters. Leave it out and a name like brisk-otter-27 is generated. |
url | Address to open at the start. Only http and https are accepted. |
extensions | The apps the session starts with, as [{ id, settings? }] (see Apps below). agent is always added. Leave it out and the session starts with your always-activate set; what your plan, your workspace or the browser refuses there is left out instead of failing the start. If an app you name is refused, the start fails, and the answer names it in extension. Your workspace's enforced apps come on either way. Name a network app either here or with the older fields below, not both. |
country | The older way of naming network.location, and it still works: a country or city code from vpnLocations in GET /image, for example de or us-dal. If no exit of that location is free right now, the session still starts, on the data centre's own connection, and the response says so with location_applied: false. Can't be combined with proxy. |
egress | residential stands for network.residential: the session leaves through a residential connection, and country chooses the country. The traffic counts towards your plan's residential allowance. Can't be combined with proxy. |
proxy | Stands for network.proxy: your own proxy as protocol://[user:password@]host:port, with http, https, socks4 or socks5. Private and internal addresses are refused. Can't be combined with country or egress. For a proxy or VPN saved in Guard.ch, name network.proxy or network.wireguard with its egressId in extensions. |
analysis | The older switch for the security apps: true starts the session with all of them, like the Security Suite. A browser without supports_analysis answers analysis_unavailable. |
screen | Viewport as {"width": 1440, "height": 900}: width 320 to 1920, height 480 to 1080. Default: 1366 x 768. |
idempotencyKey | Retry key of 1 to 128 characters. Repeat the request with the same API key and retry key within 10 minutes and you get the first successful response back instead of a second browser. Failed attempts aren't stored, so a retry after a refusal really tries again. |
For example:
curl -X POST https://api.guard.ch/v7/web/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "triage-4821",
"url": "https://example.com",
"extensions": [
{ "id": "network.location", "settings": { "country": "DE" } },
{ "id": "security.reputation" }
],
"screen": { "width": 1440, "height": 900 },
"idempotencyKey": "ticket-4821"
}'{
"status": "ok",
"id": "brl-v-v7-q8m2r7k4d1",
"name": "triage-4821",
"image": "chrome",
"connectUrl": "wss://api.guard.ch/web/brl-v-v7-q8m2r7k4d1/cdp?token=3f9c2a7d",
"liveViewUrl": "https://guard.ch/en/viewer?id=brl-v-v7-q8m2r7k4d1",
"createdAt": "2026-09-26T08:14:03.512Z",
"screen": { "width": 1440, "height": 900 },
"analysisEnabled": true,
"location_applied": true,
"quota": {
"running_session_limit": 3,
"running_sessions_used": 1,
"running_sessions_remaining": 2
}
}A successful response contains:
| Field | Description |
|---|---|
id | Session ID for the other routes. |
name | The name you gave, or the generated one. |
image | The image the session runs on. |
connectUrl | CDP WebSocket URL for Playwright or Puppeteer. It carries the session's own token, so treat it like a password. |
liveViewUrl | The session's live view in Guard.ch, for watching and taking over. Opening it requires signing in to your account. |
createdAt | Start time in ISO 8601. |
screen | The viewport the browser runs with. |
analysisEnabled | Whether the session started with the security apps on. |
location_applied, location_reason | location_applied is false when the location you asked for could not be applied, and location_reason says why: unavailable means no exit of that location was free. The session then runs on the data centre's connection. Without a location it is true. |
quota | Your sessions at once, including this one: running_session_limit, running_sessions_used and running_sessions_remaining. |
A refused start also carries a machine-readable code:
| Code | Meaning |
|---|---|
invalid_arguments | A value can't be used, for example a malformed proxy, or an app or setting the API does not accept (reason and extension say which). Most field checks answer without a code; message then names the field. |
plan_required | Your plan lacks an app you asked for (status upgrade; extension and feature name it), or your workspace switched it off (status denied, reason: workspace_policy, policy names it). |
analysis_unavailable | analysis: true on a browser that cannot run the security apps. |
image_not_supported | The image doesn't exist or can't run API sessions. allowedImages lists the images you can use. |
image_restricted | Your plan can't start this image (status upgrade). allowedImages lists the images you can use. |
quota_exceeded | All your session slots are taken or an allowance is used up. Stop a session or change the request; sending the same call again won't help. |
rate_limited | You started too many sessions in a short time. retryAfterMs says how many milliseconds to wait. |
internal_error | Something failed on our side (status error). Try again later. |
List sessions
GET https://api.guard.ch/v7/web/sessionsReturns your sessions in data, newest first. Only sessions started through the API or MCP are listed. Sessions you start by hand in Guard.ch are left out, but GET /web/sessions/:id still finds them by ID.
| Parameter | Description |
|---|---|
status | all adds ended sessions from the last 30 days. Without it, only sessions that are starting or running are listed. |
limit | Maximum number of sessions, 1 to 200. Default: 50. |
curl "https://api.guard.ch/v7/web/sessions?status=all&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"{
"status": "ok",
"data": [
{
"id": "brl-v-v7-q8m2r7k4d1",
"name": "triage-4821",
"image": "chrome",
"status": "running",
"createdAt": "2026-09-26T08:14:03.512Z",
"endedAt": null,
"endedReason": null,
"startRefusal": null,
"expiresAt": "2026-09-27T02:14:03.512Z",
"lastseen": "2026-09-26T08:31:40.087Z",
"url": "https://example.com",
"country": "de",
"location_applied": true,
"connectUrl": "wss://api.guard.ch/web/brl-v-v7-q8m2r7k4d1/cdp?token=3f9c2a7d",
"liveViewUrl": "https://guard.ch/en/viewer?id=brl-v-v7-q8m2r7k4d1",
"startedByKind": "agent",
"analysisEnabled": false,
"startedBy": { "id": 4821, "email": "ana@example.com", "firstname": "Ana", "lastname": "Meier" }
}
]
}Each session has these fields:
| Field | Description |
|---|---|
id, name, image | Session ID, display name and image, as returned when it started. |
status | starting or running while the session is live. Any other value means it has ended. |
createdAt, endedAt | Start and end time in ISO 8601. endedAt is null while the session runs. |
endedReason | Why it ended: stopped (by you or through the API), timeout (the maximum lifetime was reached), disconnect (nothing was connected for too long), runtime (your plan's runtime allowance is used up), disk (it reached its storage limit), error or lost (the browser failed or its machine went away). Null while it runs. |
expiresAt | The latest time the session can end, set by your plan. It can end earlier; see Limits. |
lastseen | When a connected client last kept the session alive. |
url | The start address from the request, or null. |
country | The location the session exits from right now, or null for the data centre's own connection. |
location_applied, location_reason | location_applied is false when the location asked for at the start could not be applied and has not been changed since; location_reason then says why (unavailable). |
connectUrl | CDP WebSocket URL while the session is starting or running, otherwise null. |
liveViewUrl | The live view in Guard.ch. |
startedByKind | agent for sessions started through the API or MCP, app for sessions started by hand in Guard.ch. |
analysisEnabled | Whether a security app is on right now. |
startedBy | The account that started the session: id, email, firstname and lastname. |
Read a session
GET https://api.guard.ch/v7/web/sessions/:idReturns one session in data, with the same fields as the list. It also finds a session you started by hand in Guard.ch, so a script can attach to it through its connectUrl. An unknown ID or another person's session answers denied.
Stop a session
DELETE https://api.guard.ch/v7/web/sessions/:idEnds the session, removes its browser and frees the slot immediately. The answer is ok even if the session had already ended. endedReason becomes stopped unless the session ended earlier for another reason. This also ends sessions you started by hand, so only stop what your script started.
curl -X DELETE https://api.guard.ch/v7/web/sessions/brl-v-v7-q8m2r7k4d1 \
-H "Authorization: Bearer YOUR_API_KEY"Download a file
GET https://api.guard.ch/v7/web/sessions/:id/downloads/:downloadIdReturns the bytes of a file the browser downloaded during the session, with its content type and file name. The MCP tool browser_downloads hands out this URL with a signed token parameter. The link itself is the credential: it opens this one file for 15 minutes without an Authorization header, so you can open it in a browser or pass it on. List the downloads again for a fresh link.
Unlike every other route, this one answers with real HTTP status codes: 200 with the file, 403 with status denied if the link has expired or isn't valid, or 404 with status denied if the file isn't available. The file is requested again from the page with its cookies, so a one-time or expired download link answers 404.
Apps
Everything optional in a session is an app from the App Store: its exit, anonymous mail, files, agent control and the security apps. A session starts with the ones you name in extensions, or with your always-activate set, and you can switch them while it runs. Your workspace can lock an app or enforce it; the API follows those rules and names the one that refused.
The apps and their settings:
| App | Description |
|---|---|
network.location | Location Switch, a data centre exit. Settings: country (two letters, US) and optionally location, a city code from GET /image (us-dal). |
network.residential | Home IP, a residential connection counted towards your allowance. Settings: country, optional. |
network.proxy | Proxy Connect, your own proxy. Settings: egressId of a proxy saved for you or your workspace, or proxy as protocol://[user:password@]host:port. |
network.wireguard | Custom VPN, your own WireGuard VPN. Settings: egressId of a saved VPN. |
mail | Anonymous Mail: your addresses and inbox in the session. |
files | Files: uploads and downloads between your device and the session. |
agent | Agent Bridge: CDP, MCP and handing the browser over. Always on in sessions started through this API. |
security.reputation, security.domain, security.privacy, security.page, security.ai | The security apps: Reputation Check, Domain Inspector, Tracker Scan, Page Guard and AI Verdict. They need an image with supports_analysis in GET /image. |
List apps
GET https://api.guard.ch/v7/extensionsWith image (a browser image ID) the answer is what a new session on that browser could use; with vm (a session ID) it is that session's current state. Send exactly one of the two.
curl "https://api.guard.ch/v7/extensions?image=chrome" \
-H "Authorization: Bearer YOUR_API_KEY"{
"status": "ok",
"extensions": [
{ "id": "network.location", "state": "available", "alwaysOn": true },
{ "id": "mail", "state": "available", "alwaysOn": false },
{ "id": "security.ai", "state": "locked", "reason": "policy", "alwaysOn": false }
],
"packs": [
{
"id": "pack.security",
"extensions": ["security.reputation", "security.domain", "security.privacy", "security.page", "security.ai"]
}
],
"alwaysOn": ["network.location"]
}| Field | Description |
|---|---|
extensions | Each app with id, state (available, active, locked or enforced), reason when locked (offered, feature, policy or image), feature for a missing plan feature, and alwaysOn. |
packs | Groups that the App Store switches on in one step, such as pack.security (Security Suite), with their extensions. To start or change a session, send the apps themselves. |
alwaysOn | Your always-activate set: what a session starts with when you send no extensions. |
canChange | With vm: whether you may switch this session's apps. |
Switch apps on
POST https://api.guard.ch/v7/vm/:vmId/extensionsSwitches apps on in a running session, or changes the settings of ones already on. The session keeps its tabs and state, and a network app replaces the active one. The answer comes once the change is applied. The body is extensions, one or more { id, settings? } from the list above.
curl -X POST https://api.guard.ch/v7/vm/brl-v-v7-q8m2r7k4d1/extensions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "extensions": [{ "id": "mail" }, { "id": "network.residential", "settings": { "country": "DE" } }] }'| Field | Description |
|---|---|
extensions | The active apps after the change, each with id, settings, activatedAt and problem. |
activated, reconfigured, deactivated | What the change switched on, changed or switched off (a new exit switches the old one off). |
egress | Only when the exit changed: auto, datacenter, residential, proxy or wireguard, with country, location, egressId and, for your own proxy or VPN that does not work, problem. |
A refused change names the extension and a reason:
| Reason | Meaning |
|---|---|
feature | Your plan lacks it (status upgrade; feature names the missing plan feature). |
workspace_policy | Your workspace locked it (policy names it). |
enforced | Your workspace enforces it, so it cannot be switched off. |
image | The session's browser cannot carry it, for example a security app on Firefox. |
invalid_settings | A setting is wrong; field names it. |
unknown, offered, unavailable | Guard.ch does not know or offer this app, or it cannot run right now. |
conflict, inactive, not_running, not_found | Two exits at once, a change to an app that is off, a session that is not running, or one that is not yours. |
A new exit can also be refused for its own reason, such as residential_limit_reached, invalid_proxy or egress_not_found.
Change an app's settings
PATCH https://api.guard.ch/v7/vm/:vmId/extensions/:idChanges the settings of an active app, for example the city of the server location. Send the new settings as the body; an app that is off answers reason: inactive. The answer is the same as for switching on.
curl -X PATCH https://api.guard.ch/v7/vm/brl-v-v7-q8m2r7k4d1/extensions/network.location \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "settings": { "country": "US", "location": "us-dal" } }'Switch an app off
DELETE https://api.guard.ch/v7/vm/:vmId/extensions/:idSwitches one app off, or every security app at once with pack.security. Switching the network app off moves the session to the data centre's own connection; an app your workspace enforces stays on (reason: enforced).
curl -X DELETE https://api.guard.ch/v7/vm/brl-v-v7-q8m2r7k4d1/extensions/pack.security \
-H "Authorization: Bearer YOUR_API_KEY"Usage and quota
API usage
GET https://api.guard.ch/v7/web/usageTotals for the sessions your account started through the API or MCP:
| Field | Description |
|---|---|
activeSessions | Sessions starting or running right now. |
sessionsLast30d | Sessions started in the last 30 days. |
totalMinutesLast30d | Their combined runtime in minutes; running sessions count up to now. |
plan | Your plan: name, runningSessionLimit (sessions at once) and featureProgrammaticAccess. |
Plan limits
GET https://api.guard.ch/v7/user/quotaThe meters your plan enforces, across every session of your workspace, whether started by hand or through the API:
| Field | Description |
|---|---|
has_plan | Whether an active plan applies. |
running_session_limit, running_sessions_used, running_sessions_remaining | Sessions at once: allowed, in use and free. |
cycle_start, cycle_session_limit, cycle_sessions_used, cycle_sessions_remaining | When the current billing cycle began, and the sessions counted in it. A limit of null means unlimited. |
residential_cycle_mb_limit, residential_bytes_used_cycle | Your workspace's residential allowance for the cycle in MB (null when unlimited, 0 without a plan) and what is used, in bytes. |
member | Your own share: the limits a manager or the plan holds you to (null when only the workspace's limit applies) and your own sessions and residential bytes. |
Images and locations
GET https://api.guard.ch/v7/imageThe catalogue behind image, the location codes and the residential exits. It needs no key.
curl https://api.guard.ch/v7/image| Field | Description |
|---|---|
images | Browser images with id, name, picture, enabled, requires_feature_restricted_images, supports_analysis (can run the security apps) and supports_programmatic. Pick one that is enabled and supports programmatic access. |
vpnLocations | Countries with code, name, default (the city a bare country code resolves to) and cities, each with code and location. Both country and city codes work as a location. |
residential | Residential coverage: configured, and countries with the country codes it can reach (null: every country). |
Limits
- 3 sessions at once per seat. Sessions you start by hand count too. The whole team shares them, and a workspace manager can limit individual members. When every slot is taken, a new session is refused, not queued.
- Sessions end on their own. A session ends a few minutes after the last client disconnects, and at
expiresAtat the latest: 18 hours after it started. Stop sessions you no longer need to free their slot right away. - Starting sessions is rate-limited. Limits per minute, hour and day apply to each API key and each account. A refusal carries
code: rate_limitedandretryAfterMs. - Requests are rate-limited too. The API host accepts about two requests per second from one IP address, with short bursts above that. Beyond it, the answer is
denied; wait a moment and try again. - Calls aren't billed. Neither requests nor sessions are charged by volume. Residential traffic counts towards your plan's allowance for the billing cycle.
Security
- An API key has the same rights as your account on these routes. Keep it in a secret store and revoke it on the Agents tab if it ever leaks.
- Send keys only in the Authorization header, never in URLs, where they end up in logs and browser history.
- Every
connectUrlcarries its own token and stops working when the session ends. Don't log or share it. - Target pages load in our infrastructure, never on the machine that calls the API.
Need help?
Questions about a route, or missing something for your integration? Get in touch.