Browser
Chrome automation as a REST API: drive a browser, scrape data, run tests.
A container’s desktop applications are reachable at HTTPS URLs. Open one in a browser on a phone, tablet, laptop, or TV and you get the running desktop, with VS Code, a web browser, LibreOffice, or any other Linux GUI program on it.
Containers created with hoody_kit: true include hoody-display, which serves that desktop over HTTP to an HTML5 client. Nothing is installed on the viewing device.
Full parameter, response, and example documentation for each endpoint lives in the API reference.
Web client interface:
dark_mode, decorations, toolbar, floating_menu, title_show_hoodyencoding, bandwidth_limit, video, offscreenkeyboard_layout, swap_keys, clipboard, keyboardsharing, steal, readonlyfile_transfer, printingdebug_main, debug_network, debug_keyboard, debug_mouseScreenshot and thumbnail API:
System information:
Mouse control:
Keyboard control:
Window management:
Clipboard:
Compound actions (Computer Use):
A container can run several display instances at once, each with its own URL:
https://{project}-{container}-display-1.{server}.containers.hoody.comhttps://{project}-{container}-display-2.{server}.containers.hoody.comhttps://{project}-{container}-display-3.{server}.containers.hoody.comThe usual arrangement is one display per application:
display-1 - Main IDE (VS Code)display-2 - Web browser (Firefox/Chrome)display-3 - Office applications (LibreOffice)display-4 - Graphics editor (GIMP)display-5 - Database toolsEach display runs independently, with its own:
Any of them opens in any browser, on a phone, tablet, laptop, or TV, with nothing to install and nothing to configure.
Terminal integration: When you create a terminal session with display: "5", the kit exports DISPLAY=:5 into that shell, connecting it to display-5. The conventional pattern is to match the terminal number to the display number (e.g. terminal-5 paired with display: "5"), but there is no automatic mapping: you must set the display field explicitly at session creation. Run firefox in such a terminal and it appears in display-5.
Manual display selection: Set the DISPLAY environment variable to target a specific display:
# In any terminalexport DISPLAY=:5
# Now GUI programs open in display-5firefox & # Opens in display-5code . # Opens in display-5Applications can be spread across displays and driven from any terminal.
The same URL works on every device that has a browser:
The browser is the only thing running on the device. Applications execute on the server, so the device’s own processor and memory do not limit what you can run.
Share the URL and everyone who opens it sees and controls the same desktop:
https://{project}-{container}-display-1.{server}.containers.hoody.com/?sharing=trueEveryone connected:
Common uses:
See: Multiplayer by Default → for the collaboration model.
The client takes its configuration from the query string:
# Read-only dashboard?readonly=true&decorations=false&toolbar=false&reconnect=true
# Low-bandwidth mode?encoding=jpeg&bandwidth_limit=1000000&video=false&sound=false
# Collaborative session?sharing=true&steal=false
# macOS user setup?swap_keys=true&keyboard_layout=us
# Dark mode with floating menu?floating_menu=true&dark_mode=trueOver 50 parameters control the UI, performance, input, feature flags, and session behavior.
See: Web Client Interface → for the full list.
Capture the current desktop state over HTTP:
# Capture current screenshotGET /api/v1/display/screenshot?displayId=1
# Get as base64 for AI visionGET /api/v1/display/screenshot?base64=true
# Lightweight thumbnailGET /api/v1/display/thumbnail/lastTypical uses:
See: Screenshot API →
A display is an ordinary URL, so it loads in an <iframe>:
<!-- Embed desktop in webpage --><iframe src="https://{project}-{container}-display-1.{server}.containers.hoody.com" width="1280" height="720" />
<!-- Multiple displays in one page --><iframe src="https://prod-container-display-1.{server}.containers.hoody.com" /><iframe src="https://staging-container-display-1.{server}.containers.hoody.com" /><iframe src="https://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com" />Composing iframes gives you a custom dashboard whose panels are live desktops rather than screenshots of them.
See: Embeddability Revolution →
RDP Client (installed) → RDP Server (configured) → Desktop (complex)Where that model gets awkward:
Any Browser → Display URL → Desktop (immediately)What changes with a display URL:
<iframe src="display-url" />A phone browser opening a display URL gets whatever runs in that desktop:
// Phone browser opens display URLhttps://{project}-{container}-display-1.{server}.containers.hoody.com
// Inside that desktop:- Full VS Code IDE- Chrome browser with DevTools- Terminal sessions- Database tools- Any Linux GUI applicationNone of these applications runs on the phone. The container runs them and the browser renders the result.
Every input a person can give a desktop is also an HTTP endpoint: mouse, keyboard, window management, and compound actions, across dozens of REST routes.
Anything that can make an HTTP request can drive a GUI application through them, whether that is an automation script, a remote operator, or an AI agent working from screenshots.
Absolute and relative cursor movement, clicks, and scrolling, at pixel precision:
# Move cursor to positionhoody display mouse move --x 640 --y 480 --display-id 10 -c <container-id>
# Click at current positionhoody display mouse click --button 1 --display-id 10 -c <container-id>
# Double-clickhoody display mouse double-click --button 1 --display-id 10 -c <container-id>
# Scroll downhoody display mouse scroll --direction down --clicks 5 --display-id 10 -c <container-id>import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Move cursor to positionawait containerClient.display.input.mouseMove({ x: 960, y: 540 }, { displayId: 10 });
// Click the left mouse buttonawait containerClient.display.input.mouseClick({ button: 1 }, { displayId: 10 });
// Double-clickawait containerClient.display.input.mouseDoubleClick({ button: 1 }, { displayId: 10 });
// Scroll downawait containerClient.display.input.mouseScroll({ direction: 'down', clicks: 3 }, { displayId: 10 });
// Get current cursor positionconst location = await containerClient.display.input.mouseLocation({ displayId: 10 });# Click the left mouse buttoncurl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.com/api/v1/display/mouse/click" \ -H "Content-Type: application/json" \ -d '{"button": 1}'
# Double-clickcurl -X POST ".../api/v1/display/mouse/double-click" \ -d '{"button": 1}'
# Move cursor to absolute positioncurl -X POST ".../api/v1/display/mouse/move" \ -d '{"x": 960, "y": 540}'
# Move cursor by relative offsetcurl -X POST ".../api/v1/display/mouse/move-relative" \ -d '{"x": 50, "y": -20}'
# Press and hold mouse button (for drag setup)curl -X POST ".../api/v1/display/mouse/down" \ -d '{"button": 1}'
# Release held mouse buttoncurl -X POST ".../api/v1/display/mouse/up" \ -d '{"button": 1}'
# Scroll downcurl -X POST ".../api/v1/display/mouse/scroll" \ -d '{"direction": "down", "clicks": 3}'
# Get current cursor positioncurl ".../api/v1/display/mouse/location"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Sends a left-click to display 1. The other mouse actions in the HTTP tab — double-click, move, scroll, and the rest — follow the same shape against their own endpoint path.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-display-1.SERVER.containers.hoody.com/api/v1/display/mouse/click&method=POST&json={"button":1}&response=transparent Available mouse buttons: 1 (left), 2 (middle), 3 (right), 4 to 7 (extra)
Type text and send any key combination the OS understands:
# Type texthoody display keyboard type --text "Hello, World!" --delay 50 --display-id 10 -c <container-id>
# Press key combinationhoody display keyboard key --keys '["ctrl+s"]' --display-id 10 -c <container-id>
# Hold keyhoody display keyboard key-down --key "Shift_L" --hold-ms 2000 --display-id 10 -c <container-id>// Type a string of textawait containerClient.display.input.keyboardType({ text: 'Hello, world!' }, { displayId: 10 });
// Press key combinations (X11 keysym notation)await containerClient.display.input.keyboardKey({ keys: ['ctrl+c'] }, { displayId: 10 });await containerClient.display.input.keyboardKey({ keys: ['ctrl+shift+t'] }, { displayId: 10 });
// Hold a key downawait containerClient.display.input.keyboardKeyDown({ key: 'Shift_L' }, { displayId: 10 });
// Release a held keyawait containerClient.display.input.keyboardKeyUp({ key: 'Shift_L' }, { displayId: 10 });# Type a string of textcurl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.com/api/v1/display/keyboard/type" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, world!"}'
# Press key combinations (X11 keysym notation)curl -X POST ".../api/v1/display/keyboard/key" \ -d '{"keys": ["ctrl+c"]}'
curl -X POST ".../api/v1/display/keyboard/key" \ -d '{"keys": ["ctrl+shift+t"]}'
curl -X POST ".../api/v1/display/keyboard/key" \ -d '{"keys": ["super+l"]}'
# Hold a key down (for sustained modifier keys)curl -X POST ".../api/v1/display/keyboard/key-down" \ -d '{"key": "Shift_L"}'
# Release a held keycurl -X POST ".../api/v1/display/keyboard/key-up" \ -d '{"key": "Shift_L"}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Types the given text into display 1. The other keyboard endpoints in the HTTP tab — key combinations, key-down, key-up — follow the same shape against their own endpoint path.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-display-1.SERVER.containers.hoody.com/api/v1/display/keyboard/type&method=POST&json={"text":"Hello,%20world!"}&response=transparent List the windows on a desktop, then focus, move, resize, or close any of them:
# List visible windowshoody display windows list --only-visible --display-id 10 -c <container-id>
# Focus a windowhoody display windows focus --window-id 83886081 --display-id 10 -c <container-id>
# Move a windowhoody display windows move --window-id 83886081 --x 100 --y 100 --display-id 10 -c <container-id>
# Resize a windowhoody display windows resize --window-id 83886081 --width 1024 --height 768 --display-id 10 -c <container-id>
# Search for windows by namehoody display windows search --pattern "Firefox" --name --only-visible --display-id 10 -c <container-id>// List all windowsconst windows = await containerClient.display.listWindows({ displayId: 10 });
// Get the currently active window IDconst activeId = await containerClient.display.input.windowActive({ displayId: 10 });
// Search windows by title patternconst matches = await containerClient.display.input.windowSearch({ pattern: 'Visual Studio Code' }, { displayId: 10 });
// Focus/activate a windowawait containerClient.display.input.windowFocus({ windowId: 12345678 }, { displayId: 10 });
// Move a window to new positionawait containerClient.display.input.windowMove({ windowId: 12345678, x: 100, y: 50 }, { displayId: 10 });
// Resize a windowawait containerClient.display.input.windowResize({ windowId: 12345678, width: 1280, height: 800 }, { displayId: 10 });
// Close a windowawait containerClient.display.input.windowClose({ windowId: 12345678 }, { displayId: 10 });# List all windowscurl "https://{project}-{container}-display-1.{server}.containers.hoody.com/api/v1/display/windows"
# Get the currently active window IDcurl ".../api/v1/display/window/active"
# Search windows by title patterncurl -X POST ".../api/v1/display/window/search" \ -H "Content-Type: application/json" \ -d '{"pattern": "Visual Studio Code"}'
# Focus/activate a windowcurl -X POST ".../api/v1/display/window/focus" \ -d '{"windowId": 12345678}'
# Move a window to new positioncurl -X POST ".../api/v1/display/window/move" \ -d '{"windowId": 12345678, "x": 100, "y": 50}'
# Resize a windowcurl -X POST ".../api/v1/display/window/resize" \ -d '{"windowId": 12345678, "width": 1280, "height": 800}'
# Minimize a windowcurl -X POST ".../api/v1/display/window/minimize" \ -d '{"windowId": 12345678}'
# Raise window to top of z-ordercurl -X POST ".../api/v1/display/window/raise" \ -d '{"windowId": 12345678}'
# Close a windowcurl -X POST ".../api/v1/display/window/close" \ -d '{"windowId": 12345678}'
# Get window position and sizecurl ".../api/v1/display/window/12345678/geometry"
# Get window titlecurl ".../api/v1/display/window/12345678/name"
# Get extended window propertiescurl ".../api/v1/display/window/12345678/properties"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists every window currently open on display 1. The window endpoints that change state — focus, move, resize, search — are POST and carry a JSON body, so they need method=POST and a json= parameter instead.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-display-1.SERVER.containers.hoody.com/api/v1/display/windows&method=GET&response=transparent Each of these endpoints combines several primitives into one atomic operation, so a click-then-type sequence costs a single round trip:
# Click at specific positionhoody display input click-at --x 640 --y 480 --button 1 --display-id 10 -c <container-id>
# Type at positionhoody display input type-at --x 300 --y 400 --text "Hello" --delay 50 --display-id 10 -c <container-id>
# Drag between positionshoody display input drag --start-x 100 --start-y 100 --end-x 300 --end-y 300 --display-id 10 -c <container-id>
# Execute action with screenshothoody display input act --action mouse/click --params button=1 --screenshot --display-id 10 -c <container-id>// Click at specific positionawait containerClient.display.input.clickAt({ x: 960, y: 540, button: 1 }, { displayId: 10 });
// Move, click, and type: fill a form fieldawait containerClient.display.input.typeAt({ x: 450, y: 320, text: 'user@example.com' }, { displayId: 10 });
// Drag from one position to anotherawait containerClient.display.input.drag({ startX: 100, startY: 200, endX: 500, endY: 200 }, { displayId: 10 });
// Execute action with post-action screenshotconst result = await containerClient.display.input.act({ action: 'mouse/click', params: { button: 1 }, screenshot: true,}, { displayId: 10 });
// Execute a sequence of actions in one callawait containerClient.display.input.batch({ actions: [ { action: 'mouse/click', params: { button: 1 } }, { action: 'wait', params: { ms: 500 } }, { action: 'keyboard/type', params: { text: 'https://hoody.com' } }, { action: 'keyboard/key', params: { keys: ['Return'] } }, { action: 'wait', params: { ms: 2000 } }, { action: 'screenshot' }, ],}, { displayId: 10 });
// Emergency: release all held inputsawait containerClient.display.input.reset({ displayId: 10 });# Move cursor and click in one callcurl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.com/api/v1/display/input/click-at" \ -H "Content-Type: application/json" \ -d '{"x": 960, "y": 540, "button": 1}'
# Move, click, and type in one operation# Useful for filling form fieldscurl -X POST ".../api/v1/display/input/type-at" \ -d '{"x": 450, "y": 320, "text": "user@example.com"}'
# Drag from one position to anothercurl -X POST ".../api/v1/display/input/drag" \ -d '{"startX": 100, "startY": 200, "endX": 500, "endY": 200}'
# Select a range via click + shift-clickcurl -X POST ".../api/v1/display/input/select" \ -d '{"x": 200, "y": 150, "endX": 800, "endY": 150}'
# Execute a single action with optional post-action screenshotcurl -X POST ".../api/v1/display/input/act" \ -d '{"action": "mouse/click", "params": {"button": 1}, "screenshot": true}'
# Wait (with optional screenshot to observe state)curl -X POST ".../api/v1/display/input/wait" \ -d '{"ms": 1500, "screenshot": true}'
# Execute a sequence of actions in one HTTP callcurl -X POST ".../api/v1/display/input/batch" \ -H "Content-Type: application/json" \ -d '{ "actions": [ {"action": "mouse/click", "params": {"button": 1}}, {"action": "wait", "params": {"ms": 500}}, {"action": "keyboard/type", "params": {"text": "https://hoody.com"}}, {"action": "keyboard/key", "params": {"keys": ["Return"]}}, {"action": "wait", "params": {"ms": 2000}}, {"action": "screenshot"} ] }'
# Emergency: release all held inputs (buttons, keys)curl -X POST ".../api/v1/display/input/reset"
# Get display dimensionscurl ".../api/v1/display/input/display-geometry"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Moves the cursor to the given coordinates and clicks in one call. The other compound actions in the HTTP tab — type-at, drag, batch, and the rest — follow the same shape against their own endpoint path.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-display-1.SERVER.containers.hoody.com/api/v1/display/input/click-at&method=POST&json={"x":960,"y":540,"button":1}&response=transparent An agent finds the browser window, navigates to a login page, fills the form, and submits it:
const base = 'https://{project}-{container}-display-1.{server}.containers.hoody.com';
// 1. Find the browser windowconst { windows } = await fetch(`${base}/api/v1/display/windows`).then(r => r.json());const browser = windows.find(w => w.name.includes('Firefox'));
// 2. Focus itawait fetch(`${base}/api/v1/display/window/focus`, { method: 'POST', body: JSON.stringify({ windowId: browser.windowId })});
// 3. Navigate to URL via address barawait fetch(`${base}/api/v1/display/input/batch`, { method: 'POST', body: JSON.stringify({ actions: [ { action: 'keyboard/key', params: { keys: ['ctrl+l'] } }, // Focus address bar { action: 'wait', params: { ms: 200 } }, { action: 'keyboard/type', params: { text: 'https://app.example.com/login' } }, { action: 'keyboard/key', params: { keys: ['Return'] } }, { action: 'wait', params: { ms: 2000 } }, // Wait for page load { action: 'screenshot' } // Verify it loaded ] })});
// 4. Fill in login formawait fetch(`${base}/api/v1/display/input/type-at`, { method: 'POST', body: JSON.stringify({ x: 640, y: 380, text: 'user@example.com' })});
await fetch(`${base}/api/v1/display/input/type-at`, { method: 'POST', body: JSON.stringify({ x: 640, y: 450, text: 'supersecretpassword' })});
// 5. Submit and capture resultconst result = await fetch(`${base}/api/v1/display/input/act`, { method: 'POST', body: JSON.stringify({ action: 'keyboard/key', params: { keys: ['Return'] }, screenshot: true })}).then(r => r.json());
// result.screenshot.image.dataUrl is the PNG as a data URI; send it to a vision model to verify the loginThese calls act on the pointer and the keyboard rather than through a CLI wrapper or a browser-specific automation driver, so the same sequence works against any Linux GUI application, in any window, from anything that can make a curl request.
The same container, opened from three devices in turn:
# Laptop: Configure displayhttps://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com/?dark_mode=true&swap_keys=true
# Phone (later): Same URL, same environmenthttps://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com/?dark_mode=true&swap_keys=true
# Tablet (during presentation): Same environmenthttps://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com/?dark_mode=trueIt is one machine reachable from a phone, laptop, tablet, or TV. Nothing is synchronized between them, because each device is attached to the same running instance.
Several people working in one desktop:
// Create the collaborative sessionconst displayUrl = 'https://{project}-{container}-display-1.{server}.containers.hoody.com';const collaborativeUrl = `${displayUrl}/?sharing=true&steal=false`;
// Send the URL to the team. Everyone sees the same desktop and can:// - Open files in the shared VS Code// - Type in the shared terminal// - Click in the shared browser// - Edit in the shared applications
// Everyone is attached to one session, in real timeA support agent joins the customer’s desktop instead of starting a screen share:
# Customer shares the display URLhttps://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com/?sharing=true
# Support agent opens it on a phone during a commute,# sees the customer's desktop, and types the fix directly
# There is no screen-share session to set up# and no need to ask "can you see my screen?"One URL keeps control, the other is read-only:
# Presenter URL (full control)?sharing=true&steal=false&readonly=false
# Viewers URL (watch only)?sharing=true&steal=false&readonly=trueThe presenter keeps control and viewers watch in real time. Useful for:
Capture the desktop and hand the image to a vision model:
// 1. Capture screenshot via HTTPconst response = await fetch( 'https://{project}-{container}-display-1.{server}.containers.hoody.com/api/v1/display/screenshot?base64=true');const { image, info } = await response.json();
// 2. Send to a vision model via Hoody AI (any provider you've configured)const analysis = await ai.chat.completions.create({ model: 'your-vision-model', messages: [{ role: 'user', content: [ { type: 'text', text: 'What errors do you see in this IDE?' }, { type: 'image_url', image_url: { url: `data:image/png;base64,${image.data}` }} ] }]});
// 3. AI describes what it seesconsole.log(analysis.choices[0].message.content);// "I see a syntax error on line 23: unclosed bracket..."With the screenshot in hand, a model can do visual debugging, UI analysis, and accessibility testing against what is actually on screen.
Three environments side by side, each of them read-only:
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;"> <!-- Production --> <iframe src="https://prod-container-display-1.{server}.containers.hoody.com/?readonly=true&toolbar=false" />
<!-- Staging --> <iframe src="https://staging-container-display-1.{server}.containers.hoody.com/?readonly=true&toolbar=false" />
<!-- Development --> <iframe src="https://PROJECT_ID-CONTAINER_ID-display-1.{server}.containers.hoody.com/?readonly=true&toolbar=false" /></div>Every panel is live, so all three environments stay visible without switching tabs.
The desktop runs on the server, so any machine with a browser and a connection reaches it: a café, a beach, an airport, a hotel room. If the laptop breaks, another device gets you back to the same session, and there is nothing on the laptop to carry.
One container runs a browser per account, each already signed in. Five team members open the same display URL and see all 20 browsers, so nobody has to be sent a password and nobody has to ask whose turn it is to post. There is nothing to synchronize between machines, because everyone is looking at one desktop.
Start coding on a laptop, continue on a tablet during the commute, finish on a phone at a café. The VS Code window, the terminal, and the open files are the same ones, because it is the same desktop rather than a synchronized copy.
An agent works against the display URL you have open. It:
The agent acts on the desktop alongside you rather than proposing edits for you to apply.
The instructor shares one display URL with 30 students. Everyone sees the same screen while the instructor demonstrates, and a student can take control when invited.
The customer sends their display URL. The agent opens it on whatever device is to hand, sees the customer’s desktop, and types the fix. There is no meeting to join and no need to talk the customer through each click.
On a fast connection:
?encoding=h264&video=trueOn a slow or metered connection:
?encoding=jpeg&video=false&bandwidth_limit=500000?readonly=true&steal=false&sharing=trueViewers can watch but cannot type or click, which rules out accidental input during a demo. The same combination suits monitoring dashboards.
macOS keyboards need the key swap:
?swap_keys=true&keyboard_layout=usThat maps Cmd to Ctrl, so copy and paste keep the keys your hands expect.
?reconnect=trueThe client reconnects by itself after an interruption. Worth setting for mobile use, where the connection moves between WiFi and cellular, and on unreliable networks generally.
Fetch thumbnails for a grid of previews, and a full screenshot only when someone looks closely:
// Get thumbnail (small, fast)const thumb = await fetch('.../api/v1/display/thumbnail/last');
// Full screenshot only when neededconst full = await fetch('.../api/v1/display/screenshot/last');A thumbnail is 320px wide where a full screenshot is 1920px, so a gallery of them costs a fraction of the bandwidth.
?sound=false&printing=false&clipboard=false&file_transfer=falseEach of these reduces bandwidth and CPU usage. Turn on only what the session needs.
Yes. The phone’s browser renders a full Linux desktop, so VS Code, LibreOffice, GIMP, and browsers all work. None of them execute on the phone; the applications run in the container and the server does the work.
Each number is a separate desktop environment with its own URL. display-1 might show VS Code, display-2 monitoring tools, display-3 browsers. They are isolated from each other, and one container can hold several of them.
The display server holds the state and synchronizes it. When one person types, the keystroke goes to the display server, which broadcasts the result to every connected client. Input is serialized, one keystroke at a time, while the visual updates reach everyone at once. It works like Google Docs, with a desktop as the shared document.
Yes. The Computer Use API exposes mouse and keyboard control over HTTP. An agent can capture screenshots with GET /screenshot to read the desktop state, move and click the mouse at exact coordinates, type text and press key combinations, manage windows (focus, resize, move, close), and chain compound actions through POST /api/v1/display/input/batch for multi-step workflows. See Computer Use API above.
With ?reconnect=true (the default) the client reconnects once your connection returns. The desktop keeps running on the server throughout, so what you lose is the view rather than the session. On reconnect you see the current state, not the state you left.
Yes. The HTML5 client maps touch input on its own: tap for click, pinch for zoom, two-finger drag for scroll. ?keyboard=true brings up a virtual keyboard, which is what makes a touch-only device usable for full desktop control.
Extensively, all through URL parameters: floating menu style, window decorations, toolbar visibility, and dark mode.
See: UI Theming →
It depends on the encoding and how much the screen changes. H264 video runs 2-5 Mbps for smooth graphics; JPEG updates on a mostly static screen run 100-500 Kbps. ?encoding=jpeg&bandwidth_limit=1000000 caps a session at 1 Mbps.
Yes. A display URL loads in an iframe like any other page. Common patterns are dashboards showing live server state, documentation with an interactive example beside the text, and customer portals with a diagnostic desktop.
Typically 50-200ms, depending on distance to the server and network quality. The H264 encoding is tuned for interactive use. That is comfortable for coding, document editing, and web browsing, and not suitable for gaming or high-frequency trading. Latency is not a current priority for Displays, and further work on it is planned.
Check the container is running:
curl "https://api.hoody.com/api/v1/containers/{id}?runtime=true" \ -H "Authorization: Bearer $HOODY_TOKEN"Verify that runtime_info.displays shows an active display with a PID.
Common causes:
Container stopped - Start it:
curl -X POST "https://api.hoody.com/api/v1/containers/{id}/start" \ -H "Authorization: Bearer $HOODY_TOKEN"Display service not started - Wait 30-60 seconds after container start for the services to initialize
Wrong display number - Check the container’s runtime_info.displays for the available display IDs
Enable reconnect:
?reconnect=trueReduce quality for stability:
?encoding=jpeg&video=falseCheck the network:
For macOS users:
?swap_keys=true&keyboard_layout=usFor other layouts:
?keyboard_layout=gb # UK?keyboard_layout=de # German?keyboard_layout=fr # FrenchEnable virtual keyboard on touch devices:
?keyboard=trueEnable the clipboard if it is disabled:
?clipboard=trueCheck browser permissions:
Some browsers restrict clipboard access for security. Copy and paste inside the remote desktop always works.
Optimize encoding:
# Fast connection?encoding=h264
# Slow connection?encoding=jpeg&video=falseReduce bandwidth:
?bandwidth_limit=500000 # 500 Kbps maxDisable high-bandwidth features:
?sound=false&video=falseCheck server load:
# Query system resourcesGET /api/v1/system/resourcesCheck the URL parameter:
?readonly=false # Enable controlVerify permissions:
Use session sharing:
?sharing=true&steal=falseWithout it, sharing=false admits only one user, and steal=true (the default) lets a new connection kick the current one out. Collaboration needs sharing=true.
Other visual services:
Browser
Chrome automation as a REST API: drive a browser, scrape data, run tests.
Terminals
Run shell commands over HTTP, from any client.
Code
VS Code over HTTP: start an IDE on demand and share the session.
Display configuration reference: