The local HTTP API
Santiago runs a local daemon that exposes an HTTP API on http://localhost:7891. Every browser action your code or AI agent performs goes through this daemon, which drives the running Camoufox browser via Playwright. This page covers the base paths, the response envelope, status and error codes, and how the daemon protects those endpoints from other tabs on your machine.
The daemon listens on localhost only — it is never exposed to the network. If you are using the packaged agent skill, it already speaks this protocol for you; see Install the agent skill. The full per-action catalog lives in the API reference.
Base URLs
Section titled “Base URLs”There are two base paths under http://localhost:7891/api:
| Purpose | Base path |
|---|---|
| Daemon management (license, profiles, update, etc.) | http://localhost:7891/api |
| Browser automation actions | http://localhost:7891/api/automation/:profileId/<action> |
Automation endpoints always target one running profile, identified by its :profileId in the path. The <action> segment is the operation — for example navigate, click, snapshot, or screenshot.
http://localhost:7891/api/automation/:profileId/<action>Set the $PROFILE convention
Section titled “Set the $PROFILE convention”Every example below uses a shell variable for the profile id so you can copy commands without editing the path each time. Export it once with the id of a running profile:
export PROFILE="3f9a1c20-7b6e-4f0a-9c2d-1e8b5a6d4c01"All later curl snippets reference $PROFILE in the URL.
The response envelope
Section titled “The response envelope”Every endpoint returns JSON in one of two shapes. On success, ok is true and the payload lives under data:
{ "ok": true, "data": { "url": "https://example.com/", "title": "Example Domain" } }On failure, ok is false and error carries a machine-readable code plus a human-readable message:
{ "ok": false, "error": { "code": "PROFILE_NOT_RUNNING", "message": "Profile is not running" } }Some actions that simply succeed without returning data respond with just { "ok": true } — for example click, hover, and scroll-to.
A first request
Section titled “A first request”Navigate the running profile to a URL. navigate returns the resulting url and page title:
curl -s -X POST "http://localhost:7891/api/automation/$PROFILE/navigate" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com"}'{ "ok": true, "data": { "url": "https://example.com/", "title": "Example Domain" } }Reading the page is just as simple. The snapshot action returns an accessibility tree where each interactive element carries a [ref=…] you can reuse in later actions:
curl -s -X POST "http://localhost:7891/api/automation/$PROFILE/snapshot" \ -H "Content-Type: application/json" \ -d '{}'{ "ok": true, "data": { "snapshot": "- heading \"Example\" [ref=e1]\n- textbox \"Email\" [ref=e2]\n..." } }You then target elements by that ref, or by a CSS selector — most interaction endpoints accept either:
curl -s -X POST "http://localhost:7891/api/automation/$PROFILE/click" \ -H "Content-Type: application/json" \ -d '{"ref":"e1"}'{ "ok": true }HTTP status codes
Section titled “HTTP status codes”The daemon pairs each error envelope with a matching HTTP status, so you can rely on the status line alone for coarse handling.
| Status | Meaning | When you see it |
|---|---|---|
200 | Success | The action ran; read data. |
400 | Bad request | A required field is missing or invalid (e.g. an empty actions array, a tab index out of range, or trying to close the last tab). |
401 | Unauthorized | Your account’s tokens are expired or invalid. The app needs to be signed in again. |
404 | Not found | The profile is not running (PROFILE_NOT_RUNNING) or there is no pending dialog (NO_DIALOG). |
500 | Action failed | The browser operation threw — the element was not found, a navigation timed out, JavaScript errored, etc. |
A 500 here is usually not a crash: it means the requested browser operation could not complete (for example, a locator timed out). The error.code tells you which action failed, and error.message carries the underlying Playwright message.
Error codes
Section titled “Error codes”Error codes are grouped by the area they come from. Codes ending in _FAILED always carry the underlying browser error in message.
Request and lifecycle
Section titled “Request and lifecycle”| Code | Status | Meaning |
|---|---|---|
PROFILE_NOT_RUNNING | 404 | The :profileId has no running browser. Launch it first. |
BAD_REQUEST | 400 | A required field was missing or invalid (e.g. actions/fields empty, tab index out of range). |
LAST_TAB | 400 | You tried to close the only open tab. Stop the profile instead. |
NO_DIALOG | 404 | You called /dialog but no alert/confirm/prompt is pending. |
Action failures
Section titled “Action failures”| Code | Action that produced it |
|---|---|
NAVIGATE_FAILED | navigate, back, forward, reload |
SNAPSHOT_FAILED | snapshot |
SCREENSHOT_FAILED | screenshot |
CLICK_FAILED | click |
HOVER_FAILED | hover |
DRAG_FAILED | drag |
SELECT_FAILED | select-option |
SELECT_COMBOBOX_FAILED | select-combobox |
TYPE_FAILED | type |
PRESS_SEQ_FAILED | press-sequentially |
KEY_PRESS_FAILED | press-key |
FILL_FORM_FAILED | fill-form |
FILL_PAGE_FAILED | fill-page |
SCROLL_FAILED | scroll-to |
MOUSE_FAILED | mouse/move, mouse/click, mouse/down, mouse/up, mouse/wheel |
TABS_FAILED | tabs (list) |
TAB_NEW_FAILED | tabs/new |
TAB_SELECT_FAILED | tabs/select |
TAB_CLOSE_FAILED | tabs/close |
EVALUATE_FAILED | evaluate |
WAIT_FAILED | wait |
BATCH_FAILED | batch |
DIALOG_FAILED | dialog |
Targeting elements
Section titled “Targeting elements”Interaction endpoints (click, hover, type, scroll-to, and similar) require either a ref (from a snapshot) or a CSS selector. If you send neither, the action fails with the message:
Either "ref" or "selector" must be providedExample: handling an error
Section titled “Example: handling an error”When an element does not exist on the page, the action fails fast and returns the matching _FAILED code with a 500:
curl -s -X POST "http://localhost:7891/api/automation/$PROFILE/click" \ -H "Content-Type: application/json" \ -d '{"selector":"#does-not-exist"}'{ "ok": false, "error": { "code": "CLICK_FAILED", "message": "locator.click: Timeout 2000ms exceeded." } }The localhost security model
Section titled “The localhost security model”Because the daemon listens on localhost, any web page open in any browser on the same machine could, in principle, try to POST to it. A permissive CORS policy does not stop this: a “simple” cross-origin POST skips the CORS preflight entirely, and even when a preflight runs, a wildcard origin still passes it. CORS only governs whether a page can read the response — not whether the request reaches the daemon.
Santiago closes that gap with two layers of protection:
- Origin check on writes. An
onRequesthook rejects cross-originPOST,PATCH, andDELETErequests based on theirOriginheader. The bundled Santiago UI is served from the same origin as the daemon, so its requests pass through; a request originating from another site is rejected. - Secret-gated shutdown. The shutdown endpoint additionally requires an internal secret in a header. It is meant to be called only by the desktop app’s tray process, never by automation.
What this means in practice:
curland local scripts work. Command-line clients and your own automation tooling do not send a browserOriginheader, so the origin check does not block them. Everycurlexample on this page works as-is against a running daemon.- The same-origin UI works. The in-app interface shares the daemon’s origin and is allowed.
- Random web pages are blocked. A page on some other site that tries to script your profiles is rejected by the
Origincheck.
Where to go next
Section titled “Where to go next”- API reference — the complete catalog of actions, fields, and example payloads.
- Launch & run profiles — start a profile so its automation endpoints become reachable.
- Best practices — pacing, fallbacks, and batching multiple actions in one call.
- Install the agent skill — let an AI agent drive this API for you.