Health, Metrics & Debugging
Section titled “Health, Metrics & Debugging”Use these endpoints to monitor the browser server, inspect performance metrics, and retrieve buffered console and network activity from running instances. History operations read from and write to persistent storage that survives instance restarts.
All browser endpoints are reached through the instance’s container hostname. Select the instance by the browser-{N} segment in the hostname; the platform derives the instance’s port and display from it. SDK and CLI clients send a browser_id to the matching browser-<N> host; a browser_id that names a different instance than the hostname is refused with 400 INSTANCE_SELECTOR_CONFLICT. On /history it also filters the entries.
Health & metrics
Section titled “Health & metrics”GET /api/v1/browser/health
Section titled “GET /api/v1/browser/health”Returns a standardized 9-field health contract for the hoody-browser service.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/healthimport { 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.kit.getHealth();Response
Section titled “Response”{ "status": "ok", "service": "hoody-browser", "built": "2026-01-15T08:00:00.000Z", "started": "2026-01-15T12:34:56.789Z", "memory": { "rss": 67108864, "heap": 16777216 }, "fds": 42, "pid": 1234, "ip": "10.0.0.5", "userAgent": "hoody-sdk/1.0"}The response body is the plain-text string Forbidden (media type text/plain), not a JSON envelope.
ForbiddenGET /api/v1/browser/metrics
Section titled “GET /api/v1/browser/metrics”Returns detailed metrics for the server and all active instances, including age buckets, oldest and newest instance ages in milliseconds, system information, and a server-side timestamp.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/metricsimport { 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.kit.getStats();Response
Section titled “Response”{ "instances": { "total": 3, "active": 2, "byAge": { "lessThan1Min": 1, "lessThan5Min": 1, "lessThan15Min": 0, "moreThan15Min": 1 }, "oldestInstance": 1800000, "newestInstance": 15000 }, "system": { "uptime": 86400, "memory": {}, "cpu": {}, "platform": "linux", "nodeVersion": "v20.10.0" }, "configuration": {}, "timestamp": "2026-01-15T12:34:56.789Z"}Debugging
Section titled “Debugging”GET /api/v1/browser/console
Section titled “GET /api/v1/browser/console”Returns buffered browser console messages (console.log, console.error, page errors) from every tab, including tabs the page or a CDP client opened, from their first load on. Keeps the last 500 entries.
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: entries from every tab are returned. 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). |
start | query | boolean | No | Controls instance creation. Three distinct states: omitted (create one if this deployment creates instances automatically), false (never create one), or true (create one even where automatic creation is turned off). The server branches on the environment variable HOODY_DISABLE_AUTO_START. Declares no schema default. |
type | query | string | No | Filter by message type (log, error, warning, info, etc.). Repeating this key in the query string is a 400 VALIDATION_ERROR (type must not be repeated). |
since | query | string | No | Only return entries at or after this time, an ISO 8601 date or date-time like 2026-10-05T12:00:00Z. Any other value (an epoch number, a word like yesterday) is a 400 VALIDATION_ERROR naming since, never an unfiltered list. Repeating this key in the query string is a 400 VALIDATION_ERROR (since must not be repeated). |
clear | query | boolean | No (default: false) | Clear the buffer after reading. true or false; any other value is a 400 VALIDATION_ERROR naming clear. Repeating this key in the query string is a 400 VALIDATION_ERROR (clear must not be repeated). |
curl 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/console?tabId=0&type=error'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.logs.listConsole({ tabId: 0, type: 'error' });Response
Section titled “Response”{ "logs": [ { "timestamp": "2026-01-15T12:34:56.789Z", "type": "log", "text": "Page loaded", "tabId": 0 } ], "count": 1, "capture": "available"}{ "error": "tabId must be a non-negative integer (got \"abc\")", "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 | tabId is not a non-negative integer, since is not an ISO 8601 timestamp, clear is not a boolean, or a single-value parameter is repeated | Fix the parameter the message names (for tabId, pass an id from /tabs or omit it for every tab) |
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 |
TAB_NOT_FOUND when a tabId was supplied but no such tab is open (details.openTabs lists the open ids); a closed tab’s retained entries appear only in the unfiltered list, each tagged with its tabId. NOT_FOUND when no tab exists at all, or no instance is running and auto-start is disabled.
{ "error": "No tab with id 5", "code": "TAB_NOT_FOUND", "details": { "tabId": 5, "openTabs": [0, 1] }}{ "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 |
{ "error": "Browser launch failed", "code": "BROWSER_LAUNCH_ERROR"}| 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 parent could not deliver the instance’s reply. UPSTREAM_RESPONSE_TOO_LARGE indicates the reply exceeded the parent’s 128 MiB response cap and was discarded whole (never truncated). PROXY_ERROR indicates the request to the instance’s control process failed at the transport (connection refused, socket reset, DNS); the underlying exception is logged, never published. Both are parent-generated: the instance may or may not have already done the work.
{ "error": "Failed to proxy the request to the browser instance", "code": "PROXY_ERROR"}{ "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 |
TIMEOUT indicates the instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). Parent-generated, with details.phase proxy and details.outcome unknown: the instance may or may not have already done the work. When this call had to start the instance and the launch itself outlasted that timeout, details.phase is launch instead: the request was not run, and the instance may still come up.
{ "error": "The browser instance did not answer within the deadline", "code": "TIMEOUT", "details": { "phase": "proxy", "outcome": "unknown" }}GET /api/v1/browser/network
Section titled “GET /api/v1/browser/network”Returns buffered network request and response entries from every tab, including the first document request of a tab the page or a CDP client opened. Keeps the last 500 entries.
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: entries from every tab are returned. 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). |
start | query | boolean | No | Controls instance creation. Three distinct states: omitted (create one if this deployment creates instances automatically), false (never create one), or true (create one even where automatic creation is turned off). The server branches on the environment variable HOODY_DISABLE_AUTO_START. Declares no schema default. |
since | query | string | No | Only return entries at or after this time, an ISO 8601 date or date-time like 2026-10-05T12:00:00Z. Any other value (an epoch number, a word like yesterday) is a 400 VALIDATION_ERROR naming since, never an unfiltered list. Repeating this key in the query string is a 400 VALIDATION_ERROR (since must not be repeated). |
clear | query | boolean | No (default: false) | Clear the buffer after reading. true or false; any other value is a 400 VALIDATION_ERROR naming clear. Repeating this key in the query string is a 400 VALIDATION_ERROR (clear must not be repeated). |
curl 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/network?tabId=0'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.logs.listNetwork({ tabId: 0 });Response
Section titled “Response”{ "logs": [ { "timestamp": "2026-01-15T12:34:56.789Z", "method": "GET", "url": "https://example.com/", "status": 200, "resourceType": "Document", "tabId": 0 } ], "count": 1}{ "error": "tabId must be a non-negative integer (got \"abc\")", "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 | tabId is not a non-negative integer, since is not an ISO 8601 timestamp, clear is not a boolean, or a single-value parameter is repeated | Fix the parameter the message names (for tabId, pass an id from /tabs or omit it for every tab) |
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 |
TAB_NOT_FOUND when a tabId was supplied but no such tab is open (details.openTabs lists the open ids); a closed tab’s retained entries appear only in the unfiltered list, each tagged with its tabId. NOT_FOUND when no tab exists at all, or no instance is running and auto-start is disabled.
{ "error": "No tab with id 5", "code": "TAB_NOT_FOUND", "details": { "tabId": 5, "openTabs": [0, 1] }}{ "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 |
{ "error": "Browser launch failed", "code": "BROWSER_LAUNCH_ERROR"}| 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 parent could not deliver the instance’s reply. UPSTREAM_RESPONSE_TOO_LARGE indicates the reply exceeded the parent’s 128 MiB response cap and was discarded whole (never truncated). PROXY_ERROR indicates the request to the instance’s control process failed at the transport (connection refused, socket reset, DNS); the underlying exception is logged, never published. Both are parent-generated: the instance may or may not have already done the work.
{ "error": "Failed to proxy the request to the browser instance", "code": "PROXY_ERROR"}{ "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 |
TIMEOUT indicates the instance did not answer within the server’s request timeout (--request-timeout, default 600000 ms). Parent-generated, with details.phase proxy and details.outcome unknown: the instance may or may not have already done the work. When this call had to start the instance and the launch itself outlasted that timeout, details.phase is launch instead: the request was not run, and the instance may still come up.
{ "error": "The browser instance did not answer within the deadline", "code": "TIMEOUT", "details": { "phase": "proxy", "outcome": "unknown" }}Browsing history
Section titled “Browsing history”GET /api/v1/browser/history
Section titled “GET /api/v1/browser/history”Returns paginated browsing history entries with optional filters. Reads from persistent storage. History is recorded for all navigations including API-triggered and manual browsing.
Through a browser-{N} service hostname the history is browser N’s only: browser_id defaults to N, and any other value is refused with 400 INSTANCE_SELECTOR_CONFLICT. Against a bare server, omitting browser_id returns every instance’s history.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
since | query | string | No | Return entries after this ISO 8601 timestamp |
domain | query | string | No | Filter by domain (exact match) |
browser_id | query | string | No | Filter by browser ID. Through a browser-{N} service hostname it may only be N (the default there). |
limit | query | integer | No (default: 50) | Maximum entries to return (1-500) |
offset | query | integer | No (default: 0) | Number of entries to skip for pagination |
curl 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/history?limit=20&offset=0'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.history.list({ limit: 20, offset: 0 });Response
Section titled “Response”{ "entries": [ { "id": "1709913600000-a3f2", "url": "https://example.com", "requestedUrl": "https://example.com", "title": "Example Domain", "domain": "example.com", "tabId": 0, "browserId": "1", "browserPort": 9223, "sessionId": "s-1709913600", "httpStatus": 200, "error": null, "source": "api", "timestamp": "2026-01-15T12:34:56.789Z", "created": true, "reused": false } ], "total": 1, "has_more": false, "limit": 20, "offset": 0}{"error":"Invalid limit/offset. limit must be 1-500, offset must be >= 0.","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 | 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 |
{"error":"History is disabled","code":"HISTORY_DISABLED"}DELETE /api/v1/browser/history
Section titled “DELETE /api/v1/browser/history”Deletes browsing history entries matching the given filters.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
before | query | string | No | Delete entries before this ISO 8601 timestamp |
browser_id | query | string | No | Delete entries for a specific browser ID only. Through a browser-{N} service hostname it may only be N (the default there). |
curl -X DELETE 'https://{projectId}-{containerId}-browser-1.{server}.containers.hoody.com/api/v1/browser/history?before=2026-01-01T00:00:00Z'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.history.clear({ before: '2026-01-01T00:00:00Z' });Response
Section titled “Response”{ "deleted": 42}{"error":"Invalid \"before\" date format. Use ISO 8601.","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 | 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 |
{"error":"History is disabled","code":"HISTORY_DISABLED"}