Guard.ch API reference
Every endpoint you can call with an app key, with its fields, examples and errors.
This page lists what the API accepts and returns. For a walkthrough from app 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/v8
- Authentication
- An app key in the Authorization header
- Format
- JSON over HTTPS
- Access
- Guard Analyst or Guard Team, also during the trial
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 app keys under Apps in your dashboard. A key acts for your account: the sessions it starts are yours, count toward your limits and appear in your session list.
Authentication
Send the key with every request:
Authorization: Bearer YOUR_API_KEYKeys start with brl-k-v7-. The API reads them only from this header, never from the URL. GET /image is the only endpoint on this page that works without a key. A key expires after one year without use, or on the fixed date chosen when it was created, whichever comes first.
Responses and errors
Every endpoint 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 option such as a location, your own proxy or residential egress. |
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.
{
"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 /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 isolated browser. Your code drives it over CDP, and a person can watch or take over in the live view. These endpoints only reach sessions of your own account, never a colleague's.
Start a session
POST https://api.guard.ch/v8/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. |
country | Location as a country or city code from vpnLocations in GET /image, for example de or us-dal. Without it, the session uses the data center's own connection. Can't be combined with proxy. |
egress | Set it to residential to leave through a residential connection instead of a data center. Add country to choose the country. The traffic counts toward your plan's residential allowance. Can't be combined with proxy. |
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. |
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 app 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/v8/web/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "triage-4821",
"url": "https://example.com",
"country": "de",
"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 },
"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 endpoints. |
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. |
quota | Your concurrent sessions 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. Most field checks answer without a code; message then names the field. |
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, an allowance is used up or your plan lacks an option you asked for. 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/v8/web/sessionsReturns your sessions in data, newest first. Only sessions started through the API or MCP are listed. Investigations 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/v8/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,
"expiresAt": "2026-09-27T02:14:03.512Z",
"lastseen": "2026-09-26T08:31:40.087Z",
"url": "https://example.com",
"country": "de",
"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": "[email protected]", "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), 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 | Location code, or null. |
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 investigations started by hand. |
analysisEnabled | Whether live analysis is switched on right now. |
startedBy | The account that started the session: id, email, firstname and lastname. |
Read a session
GET https://api.guard.ch/v8/web/sessions/:idReturns one session in data, with the same fields as the list. It also finds an investigation 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/v8/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 investigations you started by hand, so only stop what your script started.
curl -X DELETE https://api.guard.ch/v8/web/sessions/brl-v-v7-q8m2r7k4d1 \
-H "Authorization: Bearer YOUR_API_KEY"Download a file
GET https://api.guard.ch/v8/web/sessions/:id/downloads/:downloadIdReturns the bytes of a file the browser downloaded during the session, with its content type and file name. The download ID (d1, d2, ...) comes from the MCP tool browser_downloads, which hands out this URL.
Unlike every other endpoint, this one answers with real HTTP status codes: 200 with the file, 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.
Usage and quota
API usage
GET https://api.guard.ch/v8/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 (concurrent sessions) and featureProgrammaticAccess. |
pools | One entry per subscription: subscription_id, plan_id, plan_name, running_session_limit and running_sessions_used. A data pack that only funds residential traffic shows a limit of 0. |
Plan limits
GET https://api.guard.ch/v8/user/quotaThe meters your plan enforces, across every session of your account, 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 | Concurrent sessions: 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 residential allowance for the cycle in MB (null when unlimited or without a plan) and what is used, in bytes. |
pools | The same meters per subscription, with subscription_id, plan_id and plan_name. |
Images and locations
GET https://api.guard.ch/v8/imageThe catalog behind the image, country and egress fields. It needs no key.
curl https://api.guard.ch/v8/image| Field | Description |
|---|---|
images | Browser images with id, name, picture, enabled, requires_feature_restricted_images, supports_analysis 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 country. |
residential | Residential coverage: configured, and countries with the country codes it can reach (null: every country). |
Limits
- Three concurrent sessions per person. That is the limit on Guard Analyst and Guard Team, and sessions you start by hand count too. When every slot is taken, a new session is refused, not queued.
- Sessions end on their own. A session ends about five 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 app 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 toward your plan's allowance for the billing cycle.
Security
- An app key has the same rights as your account on these endpoints. Keep it in a secret store and revoke it under Apps 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 an endpoint, or missing something for your integration? Get in touch.