Browser interaction
Section titled “Browser interaction”The Hoody Browser service exposes page-level operations: navigate to a URL, evaluate JavaScript, extract page content as HTML, text, or PDF, take screenshots, capture accessibility snapshots, and drive the page with element actions. All endpoints target /api/v1/browser/<path> and are served by the browser container at https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com. Reach the service through the Hoody proxy; the management API hosts do not serve these routes.
Navigation
Section titled “Navigation”GET /api/v1/browser/browse
Section titled “GET /api/v1/browser/browse”Opens a new tab (or reuses an existing one) and navigates to a URL. By default a new tab is created and becomes active. Pass onlyIfNotExists=true to reuse a tab already on the target URL; pass active=false to keep the current active tab focused.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
start | query | boolean | No | Controls instance creation. Default mode: instances are created automatically; set to false to prevent creation. When auto-start is disabled globally, set to true to create an instance. The parameter intentionally declares no schema default: omitting it is not equivalent to sending true. The server branches on HOODY_DISABLE_AUTO_START (DISABLE ? start === 'true' : start !== 'false'), so an absent value means “auto-start unless the operator disabled it” while an explicit true means “start even though the operator disabled it”. |
url | query | string | No | The URL to navigate to. Repeating this key in the query string is a 400 VALIDATION_ERROR (url must not be repeated). A JSON body property named url is governed by the body schema. |
tabId | query | integer | No | The ID of the tab to interact with (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND (with details.openTabs) and a malformed id returns 400 VALIDATION_ERROR; the request never falls back to another tab. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
waitUntil | query | string | No | Navigation readiness to wait for. One of commit, domcontentloaded, or load (default load). networkidle is deliberately not offered. Repeating this key in the query string is a 400 VALIDATION_ERROR (waitUntil must not be repeated). |
timeoutMs | query | integer | No | Budget for viewport settling and navigation, in milliseconds. Omitted: legacy timing (Playwright’s 30 s default); the parent still bounds the request with --request-timeout. Present: returns 504 TIMEOUT (phase navigation) on expiry and the parent bounds the request by timeoutMs + 5 s. Repeating this key in the query string is a 400 VALIDATION_ERROR (timeoutMs must not be repeated). |
instanceGeneration | query | string | No | From /metadata; mismatch returns 409 INSTANCE_CHANGED (also when no instance exists; no instance is spawned). Repeating this key in the query string is a 400 VALIDATION_ERROR (instanceGeneration must not be repeated). |
active | query | boolean | No | Make the tab active (focused) after navigation. Default true. Strict boolean: only true or false. Repeating the key is not an error here; the instance reads the last occurrence. |
onlyIfNotExists | query | boolean | No | Reuse an existing tab whose URL already equals the target instead of opening a new one (the reused tab is still navigated; this is tab reuse, not a navigation dedup). Default false. Same strict boolean rule as active. |
ignoreGetParameters | query | boolean | No | Ignore query parameters when checking for existing URL. Default false. Same strict boolean rule as active. |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/browse?url=https%3A%2F%2Fexample.com&waitUntil=load"Responses
Section titled “Responses”{ "instanceGeneration": "gen_4a1c9f2b7d3e5081", "tabId": 3, "url": "https://github.com/", "requestedUrl": "https://github.com", "title": "GitHub", "httpStatus": 200, "created": true, "reused": false, "elapsedMs": 812}A required parameter is missing or a value is invalid. A missing url is VALIDATION_ERROR with the message Missing required parameter: url.
{ "error": "Missing required parameter: url", "code": "VALIDATION_ERROR"}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}The instance generation is stale (INSTANCE_CHANGED), another mutation is already in flight on this tab (TAB_BUSY), or this call had to start the instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Instance changed", "code": "INSTANCE_CHANGED", "instanceGeneration": "gen-7", "details": { "expected": "gen-6" }}The page at url answered with a file download (NAVIGATION_IS_DOWNLOAD), for example Content-Disposition: attachment. Downloads are not supported: the file is not saved anywhere. A download started by a click inside a page is dropped the same way, without an error.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
NAVIGATION_IS_DOWNLOAD | Navigation Is a Download | The url answered with a file download, which is not supported; nothing was saved | Fetch the file another way, for example with curl from the container |
An unexpected failure. details.phase indicates where it happened.
{ "error": "Automation failed", "code": "INTERNAL_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}The navigation failed with a transport-level error (NAVIGATION_ERROR, child-generated and carries instanceGeneration). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). Both parent-generated codes omit instanceGeneration. For UPSTREAM_RESPONSE_TOO_LARGE the navigation itself succeeded; only its reply was dropped.
{ "error": "Navigation error", "code": "NAVIGATION_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
With timeoutMs, the navigation or the proxy outran that budget (phase navigation or proxy). Without it, the navigation outran its default 30000 ms (phase navigation), or the instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms; phase proxy). Phase launch: this call had to start the instance and the launch outlasted the request timeout; the request was not run, and the instance may still come up.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}POST /api/v1/browser/browse
Section titled “POST /api/v1/browser/browse”Same behavior as GET /api/v1/browser/browse, but the navigation parameters are sent as a JSON body instead of a query string. Useful for scripts whose URL contains characters that are awkward to URL-encode.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
start | query | boolean | No | Controls instance creation, same semantics as on GET /api/v1/browser/browse. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL to navigate to. |
tabId | integer | No | Tab to navigate in (from /tabs); omitted uses the active tab, unknown returns 404 TAB_NOT_FOUND. |
waitUntil | string | No | Navigation readiness: commit, domcontentloaded, or load (default load). |
timeoutMs | integer | No | Budget for navigation, in milliseconds (1 to 30000). Omitted keeps legacy timing; present budgets the navigation and returns 504 on expiry. |
instanceGeneration | string | No | From /metadata; mismatch returns 409 INSTANCE_CHANGED. |
active | boolean | No | Whether the tab becomes the active one. Default true. Send a JSON boolean (the strings "true" and "false" are also tolerated); any other value returns 400 VALIDATION_ERROR. |
onlyIfNotExists | boolean | No | Reuse an existing tab already on this URL instead of opening a new one. Default false. |
ignoreGetParameters | boolean | No | Compare URLs for onlyIfNotExists with the query string stripped. Default false. |
Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/browse" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/", "waitUntil": "load", "timeoutMs": 15000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.navigate({ url: 'https://example.com/', waitUntil: 'load', timeoutMs: 15000});Responses
Section titled “Responses”{ "tabId": 2, "url": "https://example.com/welcome", "created": true, "reused": false, "requestedUrl": "https://example.com/", "title": "Example Domain", "httpStatus": 200, "instanceGeneration": "gen-7", "elapsedMs": 412}A required field is missing or a value is invalid. A missing url is VALIDATION_ERROR with the message Missing required parameter: url.
{ "error": "Missing required parameter: url", "code": "VALIDATION_ERROR"}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
Refused by the Source IP Guard: Hoody Kit programs are reached through their URLs only. The body is plain text (Forbidden), not the JSON envelope.
ForbiddenA tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}The per-request transport timer (--request-timeout, default 600000 ms) expired while the body was still arriving. Distinct from the automation timeoutMs budget, which answers 504.
{ "error": "Timeout", "code": "TIMEOUT", "details": { "phase": "proxy" }}The instance generation is stale (INSTANCE_CHANGED), another mutation is already in flight on this tab (TAB_BUSY), or this call had to start the instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Instance changed", "code": "INSTANCE_CHANGED", "instanceGeneration": "gen-7", "details": { "expected": "gen-6" }}The request body exceeded the configured cap (HOODY_MAX_BODY_SIZE, default 1 MiB). The connection is closed rather than drained.
{ "error": "Payload too large", "code": "PAYLOAD_TOO_LARGE", "details": { "phase": "validate" }}The page at url answered with a file download (NAVIGATION_IS_DOWNLOAD), for example Content-Disposition: attachment. Downloads are not supported: the file is not saved anywhere. A download started by a click inside a page is dropped the same way, without an error.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
NAVIGATION_IS_DOWNLOAD | Navigation Is a Download | The url answered with a file download, which is not supported; nothing was saved | Fetch the file another way, for example with curl from the container |
An unexpected failure. details.phase indicates where it happened.
{ "error": "Automation failed", "code": "INTERNAL_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}The navigation failed with a transport-level error (NAVIGATION_ERROR, child-generated and carries instanceGeneration). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). Both parent-generated codes omit instanceGeneration. For UPSTREAM_RESPONSE_TOO_LARGE the navigation itself succeeded; only its reply was dropped.
{ "error": "Navigation error", "code": "NAVIGATION_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
With timeoutMs, the navigation or the proxy outran that budget (phase navigation or proxy). Without it, the navigation outran its default 30000 ms (phase navigation), or the instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms; phase proxy). Phase launch: this call had to start the instance and the launch outlasted the request timeout; the request was not run, and the instance may still come up.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "navigation", "outcome": "unknown" }}JavaScript evaluation
Section titled “JavaScript evaluation”GET /api/v1/browser/eval
Section titled “GET /api/v1/browser/eval”Executes a JavaScript snippet in the context of the last active tab and returns the result. Useful for short, URL-safe scripts. For longer scripts, prefer the POST variant.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
tabId | query | integer | No | The ID of the tab to evaluate in (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
timeoutMs | query | integer | No | Time limit for the script, in milliseconds (1 to 30000, default 30000). A script still running when it is spent is stopped and the answer is 504 TIMEOUT (details.phase evaluate). Repeating this key in the query string is a 400 VALIDATION_ERROR (timeoutMs must not be repeated). |
scriptBase64 | query | boolean | No | Set to true when script is base64-encoded (a base64: prefix on the script is also accepted). Avoids URL-encoding issues for scripts with special characters. Default false. Repeating this key in the query string is a 400 VALIDATION_ERROR (scriptBase64 must not be repeated). |
script | query | string | Yes | JavaScript code to execute (can be base64-encoded). Repeating this key in the query string is a 400 VALIDATION_ERROR (script must not be repeated): two copies are two different programs and only the last would run. The rule is about the query string only; a JSON body property named script is governed by the body schema. |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/eval?script=document.title"Responses
Section titled “Responses”The result of the evaluated JavaScript expression. The exact shape depends on what the script returns (a primitive is JSON-encoded; an object or array is serialized as-is).
{ "result": "Example Domain"}A required parameter is missing or a value is invalid. A missing script is VALIDATION_ERROR with the message Missing required parameter: script.
{ "error": "Missing required parameter: script", "code": "VALIDATION_ERROR"}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
NOT_FOUND | Not Found | No browser instance runs at this address (or, on a tab route, the instance has no tab at all) | Check browser_host/browser_port, or create the instance with /start |
TAB_NOT_FOUND | Tab Not Found | A tabId was supplied but no such tab is open; details.openTabs lists the open ids | Pass a tab ID from /tabs, or omit tabId for the active tab |
The page navigated (or reloaded) while the request ran in it, so the result was lost (PAGE_NAVIGATED), or this call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Page navigated", "code": "PAGE_NAVIGATED", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_NAVIGATED | Page Navigated | The page navigated while the request ran in it; the result was lost | Wait for the navigation to finish (POST /wait with a loadState condition), then retry |
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The script threw an exception, or returned a promise that rejected (EVAL_ERROR). The tab is unaffected. Or the script ran, but its return value cannot be written as JSON, such as a BigInt or an object that refers to itself (RESULT_NOT_SERIALIZABLE); return a JSON-safe value instead, for example String(value).
{ "error": "Script error: ReferenceError: total is not defined", "code": "EVAL_ERROR", "details": { "name": "ReferenceError", "message": "total is not defined" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
EVAL_ERROR | Script Error | The script threw an exception or its promise rejected | Fix the script; details.name and details.message say what went wrong |
RESULT_NOT_SERIALIZABLE | Result Not Serializable | The script’s return value cannot be written as JSON (a BigInt or a circular reference) | Return a JSON-safe value, for example String(value) or the fields you need |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The tab’s renderer crashed (PAGE_CRASHED, child-generated) or the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
From the instance, with details.phase evaluate: the script did not finish within timeoutMs (default 30000 ms) and was stopped. On Chromium engines the script is terminated and the tab stays open (details.terminated: true). On Firefox the tab is closed, because that is the only way to end a running script there (details.tabClosed: true). details.terminated: false means the script could not be stopped, and the tab may stay busy until you close it with /tab/close. A script is stopped the same way when the caller disconnects before the answer. From the parent, with details.phase proxy: the instance did not answer within timeoutMs plus 5 s; with details.phase launch: this call had to start the instance and the launch outlasted the server’s request timeout.
{ "error": "Script did not finish within timeoutMs", "code": "TIMEOUT", "details": { "phase": "evaluate", "terminated": true }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The script did not finish within timeoutMs and was stopped | Make the script finish sooner, or raise timeoutMs (at most 30000) |
POST /api/v1/browser/eval
Section titled “POST /api/v1/browser/eval”Executes a JavaScript snippet provided in the request body. Accepts either an application/json body with { script, tabId, scriptBase64 } or a raw text/plain body containing the script directly.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
script | string | Yes (for JSON body) | JavaScript code to execute. |
tabId | integer | No | Tab to evaluate in (from /tabs); omitted uses the active tab, unknown returns 404 TAB_NOT_FOUND, malformed returns 400. |
scriptBase64 | boolean | No | Set to true when script is base64-encoded. |
timeoutMs | integer | No | Time limit for the script, in milliseconds (1 to 30000, default 30000). A script still running when it is spent is stopped and the answer is 504 TIMEOUT (details.phase evaluate). |
Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/eval" \ -H "Content-Type: application/json" \ -d '{ "script": "Array.from(document.querySelectorAll(\"a\")).map(a => a.href)" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.evaluate({ script: 'Array.from(document.querySelectorAll("a")).map(a => a.href)'});Responses
Section titled “Responses”The result of the evaluated JavaScript expression.
{ "result": ["https://example.com/a", "https://example.com/b"]}A required field is missing or a value is invalid. A missing script is VALIDATION_ERROR with the message Missing required parameter: script.
{ "error": "Missing required parameter: script", "code": "VALIDATION_ERROR"}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
Refused by the Source IP Guard: Hoody Kit programs are reached through their URLs only. The body is plain text (Forbidden), not the JSON envelope.
ForbiddenA tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}The per-request transport timer (--request-timeout, default 600000 ms) expired while the body was still arriving.
{ "error": "Timeout", "code": "TIMEOUT", "details": {}}The page navigated (or reloaded) while the request ran in it, so the result was lost (PAGE_NAVIGATED), or this call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Page navigated", "code": "PAGE_NAVIGATED", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_NAVIGATED | Page Navigated | The page navigated while the request ran in it; the result was lost | Wait for the navigation to finish (POST /wait with a loadState condition), then retry |
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The request body exceeded the configured cap (HOODY_MAX_BODY_SIZE, default 1 MiB). The connection is closed rather than drained.
{ "error": "Payload too large", "code": "PAYLOAD_TOO_LARGE", "details": {}}The script threw an exception, or returned a promise that rejected (EVAL_ERROR). The tab is unaffected. Or the script ran, but its return value cannot be written as JSON, such as a BigInt or an object that refers to itself (RESULT_NOT_SERIALIZABLE); return a JSON-safe value instead, for example String(value).
{ "error": "Script error: ReferenceError: total is not defined", "code": "EVAL_ERROR", "details": { "name": "ReferenceError", "message": "total is not defined" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
EVAL_ERROR | Script Error | The script threw an exception or its promise rejected | Fix the script; details.name and details.message say what went wrong |
RESULT_NOT_SERIALIZABLE | Result Not Serializable | The script’s return value cannot be written as JSON (a BigInt or a circular reference) | Return a JSON-safe value, for example String(value) or the fields you need |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The tab’s renderer crashed (PAGE_CRASHED, child-generated) or the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
From the instance, with details.phase evaluate: the script did not finish within timeoutMs (default 30000 ms) and was stopped. On Chromium engines the script is terminated and the tab stays open (details.terminated: true). On Firefox the tab is closed, because that is the only way to end a running script there (details.tabClosed: true). details.terminated: false means the script could not be stopped, and the tab may stay busy until you close it with /tab/close. A script is stopped the same way when the caller disconnects before the answer. From the parent, with details.phase proxy: the instance did not answer within timeoutMs plus 5 s; with details.phase launch: this call had to start the instance and the launch outlasted the server’s request timeout.
{ "error": "Script did not finish within timeoutMs", "code": "TIMEOUT", "details": { "phase": "evaluate", "terminated": true }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The script did not finish within timeoutMs and was stopped | Make the script finish sooner, or raise timeoutMs (at most 30000) |
Page content
Section titled “Page content”GET /api/v1/browser/html
Section titled “GET /api/v1/browser/html”Returns the full HTML content of the active page (equivalent to document.documentElement.outerHTML). The response body is text/html.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
tabId | query | integer | No | The ID of the tab to read from (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/html" \ -o page.htmlimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.getHtml();Responses
Section titled “Responses”The full HTML of the active page as text/html.
<!DOCTYPE html><html> <head><title>Example Domain</title></head> <body> <h1>Example Domain</h1> <p>Reserved for documentation examples.</p> </body></html>tabId is malformed (not a non-negative integer), or the instance address is missing or malformed (browser_host, browser_port). The message names the parameter.
{ "error": "Invalid tabId", "code": "VALIDATION_ERROR", "details": { "field": "tabId" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names it | Fix the named parameter and retry |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}The page navigated (or reloaded) while the request ran in it, so the result was lost (PAGE_NAVIGATED), or this call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Page navigated", "code": "PAGE_NAVIGATED", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_NAVIGATED | Page Navigated | The page navigated while the request ran in it; the result was lost | Wait for the navigation to finish (POST /wait with a loadState condition), then retry |
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The tab’s renderer crashed (PAGE_CRASHED, child-generated) or the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
The instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). details.phase is proxy, launch, or engine; details.outcome is unknown: the work may or may not have happened.
{ "error": "Timeout", "code": "TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The operation did not finish within its bound | Retry; for a slow page, load it with /browse and a larger timeoutMs first |
GET /api/v1/browser/text
Section titled “GET /api/v1/browser/text”Returns the visible text content of the active page (equivalent to document.body.innerText). The response body is text/plain.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
tabId | query | integer | No | The ID of the tab to read from (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/text"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.getText();Responses
Section titled “Responses”The visible text of the active page as text/plain.
Example Domain
Reserved for documentation examples.tabId is malformed (not a non-negative integer), or the instance address is missing or malformed (browser_host, browser_port). The message names the parameter.
{ "error": "Invalid tabId", "code": "VALIDATION_ERROR", "details": { "field": "tabId" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names it | Fix the named parameter and retry |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}The page navigated (or reloaded) while the request ran in it, so the result was lost (PAGE_NAVIGATED), or this call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE; the message names the host and port).
{ "error": "Page navigated", "code": "PAGE_NAVIGATED", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_NAVIGATED | Page Navigated | The page navigated while the request ran in it; the result was lost | Wait for the navigation to finish (POST /wait with a loadState condition), then retry |
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The tab’s renderer crashed (PAGE_CRASHED, child-generated) or the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
The instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). details.phase is proxy, launch, or engine; details.outcome is unknown: the work may or may not have happened.
{ "error": "Timeout", "code": "TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The operation did not finish within its bound | Retry; for a slow page, load it with /browse and a larger timeoutMs first |
GET /api/v1/browser/pdf
Section titled “GET /api/v1/browser/pdf”Generates a PDF of the current page. The response body is application/pdf. Optionally navigates to a URL first.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
tabId | query | integer | No | The ID of the tab to read from (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
url | query | string | No | Optional URL to navigate to before generating the PDF. Repeating this key in the query string is a 400 VALIDATION_ERROR (url must not be repeated): the parent’s rule is on the key, not on the operation, so it applies here too even though this parameter is declared inline rather than shared. |
format | query | string | No | Paper format such as A4 or Letter (default Letter). |
landscape | query | boolean | No | Use landscape orientation. Default false. |
printBackground | query | boolean | No | Include background graphics. Default false. |
margin | query | string | No | Uniform margin (e.g. 1cm, 0.5in). |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/pdf?format=A4&printBackground=true" \ -o page.pdfimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.exportPdf({ format: 'A4', printBackground: true });Responses
Section titled “Responses”The rendered page as a PDF document. The response body is raw application/pdf bytes (binary content); there is no JSON envelope.
format, margin, landscape or printBackground is not a documented value, tabId is malformed, a single-value parameter is repeated, or the instance address (browser_host, browser_port) is missing or malformed. The message names the parameter.
{ "error": "Invalid format", "code": "VALIDATION_ERROR", "details": { "field": "format" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names it | Fix the named parameter and retry |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}This call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE); the message names the host and port, nothing was started, and the start is not retried.
{ "error": "browser_port 9223 on 0.0.0.0 is already in use by another process; pick another browser_port or stop that process", "code": "PORT_IN_USE"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The page at url answered with a file download (NAVIGATION_IS_DOWNLOAD), for example Content-Disposition: attachment. Downloads are not supported: the file is not saved anywhere. A download started by a click inside a page is dropped the same way, without an error.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
NAVIGATION_IS_DOWNLOAD | Navigation Is a Download | The url answered with a file download, which is not supported; nothing was saved | Fetch the file another way, for example with curl from the container |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The instance is not headless Chromium. Firefox instances and headful instances (created with showBrowser=true) cannot export PDFs.
{ "error": "PDF export is not supported on this instance", "code": "NOT_SUPPORTED", "details": {}}The tab’s renderer crashed (PAGE_CRASHED, child-generated), the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated), or the page at url could not be loaded (NAVIGATION_ERROR, child-generated; the message names the network reason with credentials removed). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
NAVIGATION_ERROR | Navigation Error | The page at url could not be loaded | Check the URL and that the site is reachable from the browser |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
The instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). details.phase is proxy, launch, navigation, or engine; details.outcome is unknown: the work may or may not have happened.
{ "error": "Timeout", "code": "TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The operation did not finish within its bound | Retry; for a slow page, load it with /browse and a larger timeoutMs first |
Screenshots and snapshots
Section titled “Screenshots and snapshots”GET /api/v1/browser/screenshot
Section titled “GET /api/v1/browser/screenshot”Captures a screenshot of a browser tab. Optionally navigates to a URL first and waits for the page to load.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
start | query | boolean | No | Controls instance creation, same semantics as on /browse. |
url | query | string | No | The URL to navigate to before capturing. Repeating this key in the query string is a 400 VALIDATION_ERROR (url must not be repeated). |
tabId | query | integer | No | The ID of the tab to capture (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
format | query | string | No | Output format. One of png, jpeg, or base64 (default png). |
quality | query | integer | No | Image quality for the JPEG format (0 to 100). |
fullPage | query | boolean | No | Capture the entire scrollable page (default false). |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/screenshot?url=https%3A%2F%2Fexample.com&format=png&fullPage=true" \ -o page.pngimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.captureScreenshot({ url: 'https://example.com', format: 'png', fullPage: true});Responses
Section titled “Responses”The screenshot bytes. png and jpeg return image/png or image/jpeg directly; base64 returns a JSON envelope.
{ "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="}A query parameter failed validation.
{ "error": "Validation failed", "code": "VALIDATION_ERROR", "details": { "field": "format" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open (details.openTabs lists the open ids). Alternatively, no tab exists, no instance is available, and auto-start is disabled.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "details": { "openTabs": [2, 3] }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
NOT_FOUND | Not Found | No browser instance runs at this address (or, on a tab route, the instance has no tab at all) | Check browser_host/browser_port, or create the instance with /start |
TAB_NOT_FOUND | Tab Not Found | A tabId was supplied but no such tab is open; details.openTabs lists the open ids | Pass a tab ID from /tabs, or omit tabId for the active tab |
This call had to start a browser instance and its browser_port is already bound by another process (PORT_IN_USE); the message names the host and port, nothing was started, and the start is not retried.
{ "error": "browser_port 9223 on 0.0.0.0 is already in use by another process; pick another browser_port or stop that process", "code": "PORT_IN_USE"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PORT_IN_USE | Port In Use | The instance’s browser_port is already bound by another process | Pick another browser_port, or stop the process holding it |
The page at url answered with a file download (NAVIGATION_IS_DOWNLOAD), for example Content-Disposition: attachment. Downloads are not supported: the file is not saved anywhere. A download started by a click inside a page is dropped the same way, without an error.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
NAVIGATION_IS_DOWNLOAD | Navigation Is a Download | The url answered with a file download, which is not supported; nothing was saved | Fetch the file another way, for example with curl from the container |
The call had to start a browser instance, and the start failed for a reason other than a parameter you sent.
{ "error": "Instance start timed out", "code": "INSTANCE_START_TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTANCE_START_TIMEOUT | Instance Start Timeout | The child process did not finish starting within --worker-timeout | Retry; if it persists, raise —worker-timeout or check host load |
BROWSER_LAUNCH_ERROR | Browser Launch Failed | The browser process itself failed to launch | Retry with a chromiumVersion the deployment provides: a full version, a major version, or a channel tag such as stable |
CHILD_PROCESS_ERROR | Instance Initialization Failed | The instance failed to initialize for a reason other than the browser launch | Check the server log; the published message is server-authored and deliberately opaque |
The tab’s renderer crashed (PAGE_CRASHED, child-generated), the tab or browser closed while the request ran (BROWSER_DISCONNECTED, child-generated), or the page at url could not be loaded (NAVIGATION_ERROR, child-generated; the message names the network reason with credentials removed). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). For both parent codes the instance may or may not have already done the work.
{ "error": "Proxy error", "code": "PROXY_ERROR", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAGE_CRASHED | Page Crashed | The tab’s renderer crashed; the tab stays listed with crashed: true until it is closed | Close the tab with /tab/close and open the page again in a new tab |
BROWSER_DISCONNECTED | Browser Disconnected | The tab or the browser closed while the request ran | Check /tabs or /metadata; open a new tab or start the instance again |
NAVIGATION_ERROR | Navigation Error | The page at url could not be loaded | Check the URL and that the site is reachable from the browser |
UPSTREAM_RESPONSE_TOO_LARGE | Upstream Response Too Large | The instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole (the operation itself may have succeeded) | Ask for less (a smaller capture or page), or read the result another way |
PROXY_ERROR | Proxy Error | The request to the instance’s control process failed at the transport | Retry; if it persists, check the instance with /metadata or restart it |
The call had to start a browser instance, and another process was still installing the Chrome or Firefox build (or a Chrome Web Store extension) the instance needs when the wait limit (about 5 minutes) ran out. No instance was started. The response carries a Retry-After header with the number of seconds to wait.
The call had to start a headful instance (showBrowser=true, the default) and no display accepts connections (DISPLAY_UNAVAILABLE): the display server is not installed (answered at once) or the display did not come up within about 45 seconds. No instance was started, and the start is not retried. Start the instance with showBrowser=false (headless), or retry once the display runs.
{ "error": "Timed out waiting for another process to finish installing Chrome 136.0.7103.113; try again later", "code": "INSTALL_IN_PROGRESS"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSTALL_IN_PROGRESS | Install In Progress | Another process is installing a browser build or extension this instance needs | Retry after the Retry-After delay; the install finishes in the background |
DISPLAY_UNAVAILABLE | Display Unavailable | A headful instance needs a display, and none accepts connections | Start the instance with showBrowser=false (headless), or retry once the display runs |
The instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). details.phase is proxy, launch, navigation, or engine; details.outcome is unknown: the work may or may not have happened.
{ "error": "Timeout", "code": "TIMEOUT", "details": {}}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TIMEOUT | Timeout | The operation did not finish within its bound | Retry; for a slow page, load it with /browse and a larger timeoutMs first |
GET /api/v1/browser/snapshot
Section titled “GET /api/v1/browser/snapshot”Returns Playwright’s ARIA snapshot (YAML text) of the tab’s top document, with [ref=...] element references and a snapshotId. Use the refs as {ref, snapshotId} targets in /action and /wait.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
instanceGeneration | query | string | No | The instance generation from /metadata. If it no longer matches the running instance, returns 409 INSTANCE_CHANGED (also when no instance exists: details.expected is null). Repeating this key in the query string is a 400 VALIDATION_ERROR (instanceGeneration must not be repeated). |
tabId | query | integer | No | The ID of the tab to snapshot (from /tabs). Omitted: the active tab. Supplied: exactly that tab; an unknown id returns 404 TAB_NOT_FOUND and a malformed id returns 400 VALIDATION_ERROR. Repeating this key in the query string is a 400 VALIDATION_ERROR (tabId must not be repeated). |
timeoutMs | query | integer | No | Budget for the whole browser operation in milliseconds (default 10000), starting at handler entry (instance provisioning is never inside it). Repeating this key in the query string is a 400 VALIDATION_ERROR (timeoutMs must not be repeated). |
maxChars | query | integer | No | Hard cut on a line boundary; truncated reports it. A truncated excerpt may not parse as standalone YAML. Default 30000. Repeating this key in the query string is a 400 VALIDATION_ERROR (maxChars must not be repeated). |
includeValues | query | boolean | No | Include the contents of form controls in the snapshot. Default false. Set to true only when the values are known not to be sensitive. Anything other than true or false returns 400, and repeating this key in the query string is a 400 VALIDATION_ERROR (includeValues must not be repeated). |
Example
Section titled “Example”curl -X GET "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/snapshot?maxChars=30000&includeValues=false"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.getSnapshot({ maxChars: 30000, includeValues: false });Responses
Section titled “Responses”{ "instanceGeneration": "gen-7", "tabId": 2, "elapsedMs": 184, "snapshotId": "snap-7-3", "url": "https://example.com/", "title": "Example Domain", "format": "aria-yaml", "snapshot": "- banner:\n - heading \"Example Domain\" [ref=e1]\n - paragraph: \"Reserved for documentation examples.\" [ref=e2]\n- main:\n - text: \"More content\" [ref=e3]", "truncated": false, "chars": 142}A query parameter failed validation.
{ "error": "Validation failed", "code": "VALIDATION_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "validate", "outcome": "not-started" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names the parameter | Read the message: it names the parameter and the constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open. Alternatively, no instance exists and this route never auto-starts.
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "instanceGeneration": "gen-7", "details": { "openTabs": [2, 3] }}The instance generation is stale (INSTANCE_CHANGED) or another mutation is already in flight on this tab (TAB_BUSY).
{ "error": "Instance changed", "code": "INSTANCE_CHANGED", "instanceGeneration": "gen-7", "details": { "expected": "gen-6" }}An unexpected failure. details.phase indicates where it happened.
{ "error": "Automation failed", "code": "INTERNAL_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "capture", "outcome": "unknown" }}The tab or browser closed mid-operation (BROWSER_DISCONNECTED, child-generated and carries instanceGeneration). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). Both parent-generated codes omit instanceGeneration. For UPSTREAM_RESPONSE_TOO_LARGE the operation itself succeeded; only its reply was dropped.
{ "error": "Browser disconnected", "code": "BROWSER_DISCONNECTED", "instanceGeneration": "gen-7", "details": { "phase": "capture", "outcome": "unknown" }}The operation budget was spent waiting to resolve or capture. details.phase is resolve, capture, or proxy.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "capture", "outcome": "unknown" }}Element actions
Section titled “Element actions”POST /api/v1/browser/action
Section titled “POST /api/v1/browser/action”Performs a native element action: click, fill, type, press, select, check, or hover on a target. A 200 means the browser operation completed, not that the website’s transaction succeeded; observe with /wait and /snapshot.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
action | string | Yes | One of click, fill, type, press, select, check, hover. |
target | object | Yes | Element target. Exactly one of ref, selector, role, label, text, placeholder, testId must be set (otherwise 400). name is allowed only with role; snapshotId is required with ref; exact is allowed only with role, label, text, or placeholder (default true). Every target is strict: more than one match returns 409 AMBIGUOUS_TARGET. Semantic and CSS targets address the top document only; use a ref to act inside an iframe. Refs are opaque tokens: a prefix like f2 is Playwright’s frame-sequence namespace and says nothing about iframes. Pass refs back verbatim. |
instanceGeneration | string | No | From /metadata; mismatch returns 409 INSTANCE_CHANGED. |
tabId | integer | No | Tab to act on (from /tabs); omitted uses the active tab, unknown returns 404 TAB_NOT_FOUND. |
timeoutMs | integer | No | Budget for the whole browser operation in milliseconds (1 to 30000, default 10000). The body overrides the query. |
dialog | object | No | Optional DialogPolicy. Authorizes handling of one dialog raised during this action; it never requires one (no dialog returns dialog: null). A dialog that does not match, or a second dialog, is dismissed and the action fails with 409 UNEXPECTED_DIALOG. Fields: type (alert, confirm, prompt, beforeunload), response (accept, dismiss), promptText (text to enter when accepting a prompt; never echoed), message (if set, the dialog’s message must equal this exactly to be handled). |
value | string | No | fill/type: the text. Never echoed. |
delayMs | integer | No | type: delay between keys, in milliseconds (0 to 1000, default 0). Code points multiplied by delayMs must be smaller than timeoutMs (otherwise 400). |
key | string | No | press: Playwright key syntax (e.g. Enter, Control+a). Unknown returns 400. |
values | array of string | No | select: option values or labels (first match wins). No match returns 504 with details.availableOptions. |
checked | boolean | No | check: desired state (idempotent). |
button | string | No | click: mouse button. One of left, middle, right (default left). |
clickCount | integer | No | click: number of clicks (1 to 3, default 1). |
modifiers | array of string | No | click: keyboard modifiers (Alt, Control, Meta, Shift). |
Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/action" \ -H "Content-Type: application/json" \ -d '{ "action": "click", "target": { "ref": "e12", "snapshotId": "snap-7-3" }, "timeoutMs": 10000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.act({ action: 'click', target: { ref: 'e12', snapshotId: 'snap-7-3' }, timeoutMs: 10000});Responses
Section titled “Responses”{ "instanceGeneration": "gen-7", "tabId": 2, "elapsedMs": 88, "action": "click", "url": "https://example.com/next", "dialog": null}A body field failed validation.
{ "error": "Validation failed", "code": "VALIDATION_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "validate", "outcome": "not-started" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names the parameter | Read the message: it names the parameter and the constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open. Alternatively, the target was observed absent at the deadline in the same document (ELEMENT_NOT_FOUND). Alternatively, no instance exists (NOT_FOUND).
{ "error": "Element not found", "code": "ELEMENT_NOT_FOUND", "instanceGeneration": "gen-7", "details": { "phase": "actionability", "outcome": "unknown" }}The per-request transport timer (--request-timeout, default 600000 ms) expired while the body was still arriving. Distinct from the automation timeoutMs budget, which answers 504.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "proxy" }}One of INSTANCE_CHANGED, STALE_SNAPSHOT, TAB_BUSY, AMBIGUOUS_TARGET, or UNEXPECTED_DIALOG.
{ "error": "Ambiguous target", "code": "AMBIGUOUS_TARGET", "instanceGeneration": "gen-7", "details": { "phase": "resolve", "outcome": "unknown", "count": 3, "candidates": ["e12", "e13", "e14"] }}The request body exceeded the configured cap (HOODY_MAX_BODY_SIZE, default 1 MiB). The connection is closed rather than drained.
{ "error": "Payload too large", "code": "PAYLOAD_TOO_LARGE", "instanceGeneration": "gen-7", "details": { "phase": "validate" }}The action and target combination is invalid (INVALID_ACTION_TARGET).
{ "error": "Invalid action/target combination", "code": "INVALID_ACTION_TARGET", "instanceGeneration": "gen-7", "details": { "phase": "validate", "outcome": "not-started" }}An unexpected failure. details.phase indicates where it happened, and details.outcome indicates whether the mutation was dispatched (in the action/dialog phases).
{ "error": "Automation failed", "code": "INTERNAL_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "action", "outcome": "unknown" }}The tab or browser closed mid-operation (BROWSER_DISCONNECTED, child-generated and carries instanceGeneration). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). Both parent-generated codes omit instanceGeneration. For UPSTREAM_RESPONSE_TOO_LARGE the operation itself succeeded; only its reply was dropped.
{ "error": "Browser disconnected", "code": "BROWSER_DISCONNECTED", "instanceGeneration": "gen-7", "details": { "phase": "action", "outcome": "unknown" }}The automation timeoutMs budget was spent. details.phase and details.outcome indicate whether the action ran.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "actionability", "outcome": "not-started" }}POST /api/v1/browser/wait
Section titled “POST /api/v1/browser/wait”Blocks until a wait condition holds or the budget is spent. A miss returns 504 TIMEOUT (phase wait), never matched:false.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
browser_id | query | string | No | Selects the instance: SDK and CLI clients send the request to the browser-<N> service host. A value that conflicts with the host’s instance is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server it selects nothing; address instances there with browser_host + browser_port. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
condition | object | Yes | Discriminated by kind. target: {target, state} (state is attached, detached, visible, hidden, enabled, or disabled; detached and hidden are satisfied by absence). text: {target, text, match?} where match is contains (default) or equals; text is whitespace-normalized and case-sensitive, and innerText does not read an input’s value. url: exactly one of {url} (absolute, matched as normalized href) or {urlPrefix} (verbatim startsWith on href). loadState: {state} where state is domcontentloaded or load. |
instanceGeneration | string | No | From /metadata; mismatch returns 409 INSTANCE_CHANGED. |
tabId | integer | No | Tab to act on (from /tabs); omitted uses the active tab, unknown returns 404 TAB_NOT_FOUND. |
timeoutMs | integer | No | Budget for the whole browser operation in milliseconds (1 to 30000, default 10000). The body overrides the query. |
Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/wait" \ -H "Content-Type: application/json" \ -d '{ "condition": { "kind": "loadState", "state": "load" }, "timeoutMs": 15000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.browser.page.wait({ condition: { kind: 'loadState', state: 'load' }, timeoutMs: 15000});Responses
Section titled “Responses”{ "instanceGeneration": "gen-7", "tabId": 2, "elapsedMs": 1024, "matched": true, "url": "https://example.com/loaded"}A body field failed validation.
{ "error": "Validation failed", "code": "VALIDATION_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "validate", "outcome": "not-started" }}INSTANCE_SELECTOR_CONFLICT: the request came through a browser-{N} service hostname, which serves browser N only, and a browser_port, port, display, browser_id, browser_host or host in its query or JSON body names a different instance (browser_host is the one the query carries). details.field names the parameter, and nothing was run.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Validation Error | A request parameter failed validation; the message names the parameter | Read the message: it names the parameter and the constraint that failed |
INSTANCE_SELECTOR_CONFLICT | Instance Selector Conflict | The request came through a browser-{N} hostname and names a different instance | Call the other instance’s own browser-{N} hostname, or leave the parameter out |
A tabId was supplied but no such tab is open. Alternatively, no instance exists (NOT_FOUND).
{ "error": "Tab not found", "code": "TAB_NOT_FOUND", "instanceGeneration": "gen-7", "details": { "openTabs": [2, 3] }}The per-request transport timer (--request-timeout, default 600000 ms) expired while the body was still arriving. Distinct from the automation timeoutMs budget, which answers 504.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "proxy" }}One of INSTANCE_CHANGED, STALE_SNAPSHOT, or AMBIGUOUS_TARGET.
{ "error": "Stale snapshot", "code": "STALE_SNAPSHOT", "instanceGeneration": "gen-7", "details": { "reason": "navigated", "current": "https://example.com/other" }}The request body exceeded the configured cap (HOODY_MAX_BODY_SIZE, default 1 MiB). The connection is closed rather than drained.
{ "error": "Payload too large", "code": "PAYLOAD_TOO_LARGE", "instanceGeneration": "gen-7", "details": { "phase": "validate" }}An unexpected failure. details.phase indicates where it happened.
{ "error": "Automation failed", "code": "INTERNAL_ERROR", "instanceGeneration": "gen-7", "details": { "phase": "wait", "outcome": "unknown" }}The tab or browser closed mid-operation (BROWSER_DISCONNECTED, child-generated and carries instanceGeneration). The parent proxy may also answer 502 without ever reaching the child: UPSTREAM_RESPONSE_TOO_LARGE (the instance’s reply exceeded the parent’s 128 MiB response cap and was discarded whole), or PROXY_ERROR (the request to the instance’s control process failed at the transport; the exception text is logged rather than published). Both parent-generated codes omit instanceGeneration. For UPSTREAM_RESPONSE_TOO_LARGE the operation itself succeeded; only its reply was dropped.
{ "error": "Browser disconnected", "code": "BROWSER_DISCONNECTED", "instanceGeneration": "gen-7", "details": { "phase": "wait", "outcome": "unknown" }}The condition did not become true before the budget elapsed. details.phase is wait or proxy.
{ "error": "Timeout", "code": "TIMEOUT", "instanceGeneration": "gen-7", "details": { "phase": "wait", "outcome": "unknown" }}