Best practices & stealth
These are the rules that keep Santiago automation both fast and undetectable. Follow them and a typical form flow runs in a few hundred milliseconds per page with humanized, trusted input; ignore them and you either leak isTrusted: false events to antibot systems or waste tens of seconds on blind waits.
All examples target the local daemon. Set your profile ID once, then call only /api/automation/$PROFILE/* actions:
PROFILE=<profile-id>The response envelope is always { "ok": true, "data": {...} } or { "ok": false, "error": { "code", "message" } }.
Stealth: evaluate is read-only
Section titled “Stealth: evaluate is read-only”evaluate runs page JavaScript through the debugging protocol. The protocol itself is invisible to the page, but what your code does inside evaluate is fully visible to site scripts. Use it to read, never to interact.
| Safe (invisible to the page) | Forbidden (detectable, bypasses humanize) |
|---|---|
document.title | element.click() |
querySelectorAll, attribute values | element.focus() |
getBoundingClientRect() (element coords) | element.dispatchEvent(...) |
| computed styles | setting element.value = ... |
reading document.activeElement, aria-expanded | programmatic form.submit(), scrolling |
A synthetic element.click() produces a MouseEvent with isTrusted: false, clientX/Y: 0, and no preceding mousemove / pointerdown / mouseover. Modern antibot systems flag isTrusted: false directly. When the profile has humanize enabled, Camoufox adds bezier-curve cursor movement to native Playwright actions only — anything you do via evaluate bypasses it entirely.
# OK -- evaluate stays read-onlycurl -s localhost:7891/api/automation/$PROFILE/evaluate -X POST \ -H 'Content-Type: application/json' \ -d '{"code":"JSON.stringify(document.querySelector(\"#btn\").getBoundingClientRect())"}'
# Trusted, humanized click on the coords you just readcurl -s localhost:7891/api/automation/$PROFILE/mouse/click -X POST \ -H 'Content-Type: application/json' -d '{"x":512,"y":340}'# curl ... /evaluate -d '{"code":"document.querySelector(\"#btn\").click()"}'# curl ... /evaluate -d '{"code":"document.querySelector(\"input\").value = \"x\""}'# curl ... /evaluate -d '{"code":"form.submit()"}'fill-page is the primary form filler
Section titled “fill-page is the primary form filler”fill-page is the default choice for any form. It bypasses Playwright locators entirely (evaluate → mouse.click → keyboard.type), so it works on complex, animated and transitioning pages where ref-based actions stall on stability checks. It handles text inputs and combobox dropdowns, and includes the submit in the same call.
curl -s localhost:7891/api/automation/$PROFILE/fill-page -X POST \ -H 'Content-Type: application/json' -d '{ "fields": [ {"selector": "#firstName", "value": "Daniel"}, {"selector": "#lastName", "value": "Moreno"}, {"selector": "[role=combobox]", "value": "March", "type": "combobox", "nth": 0}, {"selector": "[role=combobox]", "value": "Male", "type": "combobox", "nth": 1} ], "submit": {"text": "Next"}, "waitAfterSubmit": 2500 }'# Response: { results: [{selector, type, ok, selectedValue?}], submit: {ok, url} }Field types:
"text"(default): click center → Ctrl+A → Backspace →keyboard.type."combobox": click to open → find option by text → click option → verify and auto-retry.nth(0-based): whenselectormatches multiple visible elements, pick the Nth one —[role=combobox]withnth: 0is the first,nth: 1the second.
Use batch for non-form actions (clicking links, hover, navigate, wait) or when you need ref-based targeting from a snapshot. fill-form is legacy — it tries a locator first (2s) and falls back to coords; prefer fill-page.
See Filling forms for a complete worked flow.
Native <select> vs ARIA combobox
Section titled “Native <select> vs ARIA combobox”How the dropdown appears in the snapshot decides the endpoint.
| Snapshot shows | Use | Behavior |
|---|---|---|
<select> (native element) | select-option with values[] | sets the native value directly |
combobox role (ARIA) | select-combobox with value | click → wait for listbox → click option, one call |
curl -s localhost:7891/api/automation/$PROFILE/select-option -X POST \ -H 'Content-Type: application/json' -d '{"ref":"e2","values":["California"]}'curl -s localhost:7891/api/automation/$PROFILE/select-combobox -X POST \ -H 'Content-Type: application/json' -d '{"ref":"e5","value":"March"}'Some sites render the listbox in a detached overlay outside the combobox’s aria-controls target. select-combobox then times out with “locator #cN is hidden”. Do not retry select-combobox — instead open the dropdown with a real click, read the option’s center coordinates via evaluate (reading only), then mouse/click those coordinates (trusted, humanized):
# 1. Open the dropdown with a real clickcurl -s localhost:7891/api/automation/$PROFILE/click -X POST \ -H 'Content-Type: application/json' -d '{"ref":"e102"}'
# 2. Read the target option's center -- evaluate stays read-onlycurl -s localhost:7891/api/automation/$PROFILE/evaluate -X POST \ -H 'Content-Type: application/json' \ -d '{"code":"const o = Array.from(document.querySelectorAll(\"li[role=option]\")).filter(e => e.offsetParent !== null).find(e => e.textContent.trim() === \"June\"); const r = o.getBoundingClientRect(); JSON.stringify({x: r.x + r.width/2, y: r.y + r.height/2})"}'
# 3. Click the coords with the mouse (trusted, humanized)curl -s localhost:7891/api/automation/$PROFILE/mouse/click -X POST \ -H 'Content-Type: application/json' -d '{"x":512,"y":340}'One snapshot, one batch — re-snapshot only on page change
Section titled “One snapshot, one batch — re-snapshot only on page change”Take one snapshot, identify all the elements you need, then send one batch call with every action. Never re-snapshot between actions on the same page.
snapshot (~110ms) -> batch of N actions (N x ~200ms) -> doneOnly take a new snapshot when the page fundamentally changes:
- navigation to a new URL,
- form submission that loads a new page,
- a modal or overlay that replaces page content.
Do not re-snapshot after filling fields, opening dropdowns, typing, or hovering. Refs (e1, e2, …) stay valid until the page changes; they only go stale after navigation, submission to a new URL, reload, or DOM-replacing dynamic content.
A batch stops on the first error — if an action fails (wrong ref, element not found), the remaining actions are skipped. Inspect the results array ({ index, action, ok, data?, error? }) to find the failed action, fix it, and re-run from that point.
Never add sleep
Section titled “Never add sleep”Playwright auto-waits for elements before acting, and the batch endpoint already adds random 80–250ms delays between actions for human-like pacing. Wrapping calls in sleep 1 / sleep 2 && curl … is pure waste that compounds into tens of seconds across a flow.
If a flow feels more than ~2× slower than expected, the cause is almost always an added sleep or a mid-page re-snapshot — remove both.
When you genuinely need to wait for a specific signal, use wait with a condition, never a blind timeout:
curl -s localhost:7891/api/automation/$PROFILE/wait -X POST \ -H 'Content-Type: application/json' -d '{"text":"Success"}'
curl -s localhost:7891/api/automation/$PROFILE/wait -X POST \ -H 'Content-Type: application/json' -d '{"selector":".loaded","state":"visible"}'
curl -s localhost:7891/api/automation/$PROFILE/wait -X POST \ -H 'Content-Type: application/json' -d '{"selector":".spinner","state":"hidden"}'A retry must change strategy
Section titled “A retry must change strategy”If an action failed, retrying with the same parameters fails the same way. Every retry must change something — a different endpoint, a different selector, keyboard instead of mouse. Re-reading the current error before retrying is mandatory.
The clearest example: select-combobox times out on a hidden #cN listbox. Do not fire it again — switch to the open-click → read-coords → mouse/click strategy above. The action timeout is 2 seconds (not Playwright’s default 30s), so failed attempts are cheap; spend that cheapness on a new approach, not the same one five times.
Debug checklist before you change strategy
Section titled “Debug checklist before you change strategy”One read-only evaluate returning these four values tells you which strategy will work, before you burn retries:
document.activeElement— did focus land where you expected?aria-expandedon the combobox — did it actually open?aria-activedescendant— which option is highlighted?getBoundingClientRect().yvswindow.innerHeight— is the target even in the viewport?
Keyboard type-ahead for stubborn dropdowns
Section titled “Keyboard type-ahead for stubborn dropdowns”When fill-page / select-combobox don’t fit (custom combobox markup, scrollable listboxes whose options live outside the viewport, mouse clicks that fail to focus), switch to the keyboard first, not to coord-hacking. Mouse coord-clicks on listbox options should be a last resort — they require the option to be inside the viewport and inside the listbox’s own clip rect, and scrolling a listbox is unreliable.
- Type-ahead on comboboxes — open with
Enteror a click, thenpress-keythe target value character-by-character (1,9,9,3for year1993). Most ARIA comboboxes jump straight to the match, skipping anyscrollTopgymnastics. Commit withEnter. Tabto traverse focus — when a click refuses to focus an element, or the submit button is below the viewport and wheel scroll is intercepted,Tabfrom the last known focused element untildocument.activeElement.textContentmatches, then activate withEnter.ArrowDown× N +Enter— works for short option lists (≤5 items, e.g. Gender: Female / Male / Custom).
# Open the combobox with a real click (trusted)curl -s localhost:7891/api/automation/$PROFILE/click -X POST \ -H 'Content-Type: application/json' -d '{"ref":"e12"}'
# Type-ahead the value, one key at a timefor k in 1 9 9 3; do curl -s localhost:7891/api/automation/$PROFILE/press-key -X POST \ -H 'Content-Type: application/json' -d "{\"key\":\"$k\"}"done
# Commit the selectioncurl -s localhost:7891/api/automation/$PROFILE/press-key -X POST \ -H 'Content-Type: application/json' -d '{"key":"Enter"}'Quick reference
Section titled “Quick reference”| Symptom | Do this |
|---|---|
| ”Ref eN not found” | Page changed. Re-snapshot. |
select-option fails: “not a select element” | It’s an ARIA combobox — use select-combobox. |
select-combobox times out on hidden #cN | Detached overlay. Don’t retry — open with click, read coords via evaluate, mouse/click. |
locator.scrollIntoViewIfNeeded: Timeout | Element is animating. Use fill-page (coord-based); switch the whole page. |
| Snapshot too large | Scope it: {"selector":"#main","depth":5}. |
| Element not visible | mouse/wheel {"deltaY":500}, then re-snapshot. |
| Flow feels >2× slow | You added sleep or re-snapshotted mid-page. Remove both. |
Need to click something found via evaluate | Read its getBoundingClientRect(), then mouse/click {x, y}. Never .click() in evaluate. |
Related pages
Section titled “Related pages”- Automation overview — what the automation API is and how it fits the daemon.
- HTTP API basics — base URL, the response envelope, and profile scope.
- API reference — every endpoint and its parameters.
- Launch profiles — get a profile running before you automate it.
- Filling forms — these rules applied end-to-end.