Hoody Code Orchestrator
Section titled “Hoody Code Orchestrator”The Hoody Code orchestrator is the HTTP entry point that fronts a container’s VS Code instances. It serves the iframe shell that loads each instance, the PWA manifest for installing the editor as an application, and a small set of static assets (favicon, robots policy, security contact). It also answers discovery requests for the orchestrator’s own OpenAPI specification.
Editor
Section titled “Editor”GET /api/v1/code
Section titled “GET /api/v1/code”Spawns or reuses a Hoody Code instance and renders it inside an iframe wrapper. The handler also answers at the root path / as an alias; both URLs reach the same code and share the query size limit described below.
The path answers with one of two bodies, chosen by whether the instance parameters carry non-empty values:
- With non-empty
folderand non-emptyid, it spawns or reuses an instance and returns an HTML page that embeds it. - With neither
foldernoridcarrying a non-empty value, it returns the orchestrator’s OpenAPI specification asapplication/x-yamland starts nothing. This is the discovery form of the path. - With exactly one of the two carrying a non-empty value, it returns
400.
A parameter sent with an empty value is treated as though the name had not been sent at all. This preserves the behaviour of older clients:
?folder=&id=returns the specification, and so does any query in which everyfolderandidvalue is empty. Nothing is started and no parameter error is reported.?id=&id=also returns the specification. Repeated empty values are not a duplicate selector.?folder=/srv/project&id=is handled as an editor request, becausefoldercarries a value, and is then rejected with400because the selector is not an unsigned decimal integer.?folder=&id=1is likewise handled as an editor request and then rejected with400for the missing folder.
When the instance is requested, the orchestrator spawns it if it does not exist or reuses it if it does. The iframe URL is rewritten to use the child’s -http-{port} subdomain. A loading overlay is shown only for fresh spawns, not for requests answered from an already-running instance.
The query string of a request to this operation is limited to 8192 UTF-8 bytes (measured after ? and before any #). The limit applies to every request to this operation, the discovery form included. A longer query is answered with 400 and no instance is started.
Only explicitly allowlisted parameters are forwarded to child instances. All other query parameters are silently ignored.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
folder | query | string | No | Absolute path to the folder to open in the instance. |
id | query | integer | No | Instance selector. |
extension | query | string | No | Extension identifier to open in extension-only mode. |
restart | query | boolean | No | Force restart the instance before rendering. |
page-loader | query | boolean | No | Enable the page loader overlay in the child instance. |
disable-walkthroughs | query | boolean | No | Disable VS Code walkthrough functionality in the child instance. |
hoody-code | query | boolean | No | Enable loading of Hoody Code injected scripts. |
welcome-iframe-url | query | string | No | URL for a custom welcome page iframe. |
proxy-domain | query | string | No | Domain pattern for port proxying. |
locale | query | string | No | Display language for VS Code UI. |
app-name | query | string | No | Custom application name shown in the VS Code title bar. |
page-loader-path | query | string | No | Path to the loading page the instance serves while it starts. Passed to child as --page-loader-path <path>. It is read by the instance, so the path is resolved on the container’s filesystem and not on the caller’s. It has no effect unless page-loader is also enabled. |
folder
Section titled “folder”Absolute path to the folder to open in the instance. Supply it together with id to open an editor. Omit both, or send both with empty values, to retrieve this specification.
The path is normalised before use. A .. segment is resolved away rather than rejected, and a relative path is resolved against the orchestrator’s own working directory, so the folder that opens may differ from the string sent. Send an absolute, already normalised path.
An empty value counts as not sent. On its own it produces the discovery response rather than an error; alongside a non-empty id it is rejected with 400.
Switching folders reuses the running instance
Section titled “Switching folders reuses the running instance”A later request naming the same id and a different folder is answered from the running instance, and the page it returns loads the editor on the folder this request names. No restart is needed. The instance keeps the folder it was started with as its own: that is the folder the status endpoint reports.
restart is optional here. It kills and respawns the instance, ending its running sessions, and applies the parameters of the request that carries it.
Instance selector. Supply it together with folder to open an editor. Omit both, or send both with empty values, to retrieve this specification.
It determines:
- TCP port:
basePort + id - Data directory:
dataDir/instances/{id}/ - Unique isolation per ID
The instance binds basePort + id, so the largest accepted value is 65535 - basePort, not a fixed number. basePort is part of this deployment’s configuration and is reported as orchestrator.basePort by /status. With a base port of 7000, for example, ids above 58535 are rejected. A rejection names the limit and the base port in use.
The value is read strictly:
- The value must be an unsigned decimal integer. A sign, a decimal point, surrounding whitespace, hexadecimal notation or any trailing character is rejected, so
+2,2.0,2,0x2and2abcare not accepted as2. - Only the exact name
idis read. Bracket spellings such asid[]andid[0]are different names: they are ignored rather than merged into this parameter. - Sending
idmore than once is rejected outright rather than resolved to one of the values. A percent-encoded spelling of the same name counts as a repeat. Repeats whose values are all empty are the exception: with no non-emptyfolderalongside them they count as no selector at all; alongside a non-emptyfolderthey are still a repeat and are rejected. - The query string carrying the selector is limited in size. See the “Query size limit” section in the operation description.
extension
Section titled “extension”Extension identifier to open in extension-only mode. Format: PUBLISHER.NAME (for example ms-python.python).
This parameter is preserved in the iframe URL for VS Code to consume and is not forwarded to the child CLI arguments. When present, VS Code hides the file explorer, focuses on the extension’s UI, and displays only that extension’s views and commands.
restart
Section titled “restart”Force restart the instance before rendering. Accepted truthy values: true, 1, yes, on.
If the instance is running and restart is explicitly true, the instance is killed, a new instance is spawned, and the iframe is rendered with the new instance. A missing or empty parameter does not trigger restart.
page-loader
Section titled “page-loader”Enable the page loader overlay in the child instance. Boolean flag (passed to child without value):
- Truthy:
true,1,yes,on, or empty string - Falsy:
false,0,no,off, or omitted
disable-walkthroughs
Section titled “disable-walkthroughs”Disable VS Code walkthrough functionality in the child instance. Boolean flag (passed to child without value). Default in orchestrator: true (walkthroughs disabled by default).
hoody-code
Section titled “hoody-code”Enable loading of Hoody Code injected scripts (extra/injected/*.js). Boolean flag (passed to child without value). When enabled, all .js files in extra/injected/ are loaded after page load.
welcome-iframe-url
Section titled “welcome-iframe-url”URL for a custom welcome page iframe. Passed to the child as --welcome-iframe-url <url>. Replaces the default Welcome (Getting Started) page with a fullscreen iframe.
proxy-domain
Section titled “proxy-domain”Domain pattern for port proxying. Automatically computed from the request Host header:
<proj>-<cont>-ui.<domain>becomes<proj>-<cont>-http-{{port}}.<domain>- Passed to child as
--proxy-domain <pattern>
Manual override: --proxy-domain custom-{{port}}.example.com.
locale
Section titled “locale”Display language for VS Code UI. Format: IETF language tag (for example en, fr, de, ja, zh-CN). Passed to the child as --locale <tag>.
app-name
Section titled “app-name”Custom application name displayed in the VS Code title bar and branding. Passed to the child as --app-name <name>. Replaces {{app}} placeholders in templates.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”With non-empty folder and id, an HTML page containing:
- A full-viewport iframe pointing to the child instance.
- A loading overlay, when this request is the one waiting for the instance to start. That includes a request that did not start it but arrived while someone else’s start was still in flight: it waits for that start and is answered with the overlay. A request answered from an instance already running has no overlay.
- A title sync script to mirror the child title.
- The favicon from the orchestrator.
With neither folder nor id carrying a non-empty value, the orchestrator’s OpenAPI specification served as application/x-yaml. No instance is spawned.
The two bodies are presented in separate tabs.
<!DOCTYPE html><html lang="en"><head> <title>Hoody Code - Instance 1</title> <link rel="icon" href="/favicon.ico"></head><body> <div class="loading"> <div class="spinner"></div> <div class="loading-text">Connecting to Hoody Code instance 1...</div> </div> <iframe id="main-iframe" src="//abc-xyz-http-7001.domain/api/v1/code?folder=%2Fhome%2Fuser%2Fproject&id=1"></iframe></body></html><!DOCTYPE html><html lang="en"><head> <title>Hoody Code - Instance 1</title> <link rel="icon" href="/favicon.ico"></head><body> <iframe id="main-iframe" src="//abc-xyz-http-7001.domain/api/v1/code?folder=%2Fhome%2Fuser%2Fproject&id=1"></iframe></body></html>openapi: 3.1.0info: title: Hoody Code API version: 2.0.0Invalid request parameters. The body is an HTML error page that names the reason.
Causes:
- Exactly one of
folderandidcarried a non-empty value. The two are supplied together or not at all. folderwas empty whileidcarried a value.idwas empty, or was repeated with empty values, whilefoldercarried a value.idis not an unsigned decimal integer, or carries a sign, a decimal point or trailing characters.idwas sent more than once under that exact name.idis larger than65535 - basePortfor this deployment.- The query string is longer than
8192bytes. This cause applies whichever parameters were sent, including none.
A .. segment in folder is not one of the causes. Such a path is normalised and accepted. Neither is a query in which every folder and id value is empty: that is the discovery form and answers 200.
The three named examples below cover the most common causes.
<!DOCTYPE html><html><body><div class="error-container"> <h1>Error</h1> <p>Invalid parameters. Required: ?folder=/path&id=number</p></div></body></html><!DOCTYPE html><html><body><div class="error-container"> <h1>Error</h1> <p>Invalid parameters: parameter 'id' must be an unsigned decimal integer</p></div></body></html><!DOCTYPE html><html><body><div class="error-container"> <h1>Error</h1> <p>Invalid parameters: parameter 'id' given 2 times</p></div></body></html>Neither folder nor id carried a non-empty value, so this specification was requested, and the specification file is not present alongside the running binary.
OpenAPI specification not foundThe instance cannot start because the port it needs, basePort + id, is already held by a process this server did not start.
Distinct from 503 on purpose: retrying does not help until whatever holds the port releases it, and the body names the port so an operator can find it.
Internal server error during instance spawn.
<!DOCTYPE html><html><body><div class="error-container"> <h1>Error</h1> <p>Failed to spawn instance: Error message here</p></div></body></html>The instance was spawned and did not finish starting within its budget, or the startup was cancelled. Nothing is left running.
Worth retrying, unlike 409 and 400. A request that arrives while another request’s startup for the same instance is still in flight joins that startup and receives its outcome, so two callers racing for one instance both see this rather than one seeing it and the other waiting on a child that is gone.
SDK and URL
Section titled “SDK and URL”import { HoodyClient } from 'hoody-sdk';
const hoody = await HoodyClient.login('https://api.hoody.com', { username, password });const box = await hoody.withContainer(containerId);
// Builds the URL of instance 1 on /home/user/project; nothing is fetched.// Opening it in a browser spawns or reuses the instance.const url = box.embeds.code.editor(undefined, { index: 1, params: { folder: '/home/user/project' },});https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/api/v1/code?folder=%2Fhome%2Fuser%2FprojectThe edge proxy sets id from the code-1 hostname, so the URL does not carry it.
DELETE /api/v1/code
Section titled “DELETE /api/v1/code”Stops one instance and ends everything running in it: open editors, integrated terminals, tasks and the extension host. Its data directory (settings, installed extensions, workspace state) is kept, so the next GET /api/v1/code for the same instance starts it again from there.
This operation never starts an instance. Asking to stop an instance that is not running answers 404 with the code unknown-instance, and nothing is spawned to answer it.
An instance that is still starting counts as running and its startup is cancelled. A request that was waiting on that startup is answered 503, unless the startup had already finished, in which case it received a page for the instance this request then stopped. A restart (restart=true) in progress counts the same way, including while its old child is still exiting: this request answers 200, the replacement is not started, and the restart request is answered 503.
The answer is 200 only once the instance’s process has been seen to exit. If it cannot be confirmed to have exited even after being killed, the answer is 500 and the instance keeps its place in the deployment’s instance limit; send the request again to retry. Until that process has exited, GET /api/v1/code for the same instance starts nothing and answers 503.
Inside a Hoody container the code-{N} hostname selects the instance, as for GET /api/v1/code: the edge proxy sets id to N and overrides any value sent. To stop instance 5, send this request to the code-5 service URL.
An instance whose editor has had no open connection for the deployment’s idle timeout is stopped as if by this operation. The timeout is three hours unless the operator set another or turned it off. Only the editor’s own report of inactivity stops an instance this way, and any request for the instance that reaches the service while it decides, or in the two minutes before, keeps it running.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | integer | Yes | Which instance to stop. On a code-{N} service URL the edge proxy sets it from the hostname and overrides any value sent, so a caller there neither needs to send it nor can change it. It is required: there is no default instance to stop, so a request without it is answered 400. Read as strictly as the selector on GET /api/v1/code: an unsigned decimal integer, under the literal name id only, never given more than once, and at most 65535 - basePort. |
Response
Section titled “Response”The instance was running, or starting, and has stopped. state is always stopped: the process has exited, or was killed after a grace period, and the instance’s port is free again.
{ "id": 5, "state": "stopped"}The instance selector was missing or unusable: absent, not an unsigned decimal integer, given more than once, above 65535 - basePort, or in a query longer than 8192 bytes.
{ "error": "invalid-selector", "message": "Invalid parameters: missing required parameter 'id'"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
invalid-selector | Invalid instance selector | The id selector was missing or unusable. | Send id as one unsigned decimal integer, or call the code-{N} service URL. |
No instance with this id is running, starting or still exiting. Nothing was started and nothing changed.
{ "error": "unknown-instance", "message": "No instance 7 is running."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
unknown-instance | Instance not running | No instance with this id is running, starting or still exiting. Nothing was started. | Check the instance id; GET /api/v1/code starts an instance. |
The request could not be completed for a reason on this side. This includes an instance that was killed but could not be confirmed to have exited: it still counts against the instance limit, and repeating the request tries again.
{ "error": "internal-error", "message": "Instance 5 could not be confirmed stopped. Retry the request."}Any other unexpected failure:
{ "error": "internal-error", "message": "The request could not be completed."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal-error | Internal error | The request could not be completed on the server side, including an instance that could not be confirmed to have exited. | Send the request again. |
curl -X DELETE "https://{projectId}-{containerId}-code-5.{server}.containers.hoody.com/api/v1/code" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-code-5.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// On a code-5 host the proxy sets id to 5 whatever is sent.const { data } = await client.code.stop({ id: 5 });console.log(data?.state); // "stopped"hoody code stop 5GET /api/v1/code/manifest.json
Section titled “GET /api/v1/code/manifest.json”The PWA manifest for this origin, so Hoody Code can be installed as an application.
The manifest is generated for this host rather than forwarded from the editor instance. The instance serves a manifest for its own origin using relative icon URLs and start_url: "."; served from here those would resolve to paths this host does not serve, producing a manifest that installs a broken application.
This endpoint takes no parameters.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”A web application manifest whose required fields are id, name, short_name, start_url, scope, display and icons. start_url is an absolute path on this origin, never a relative one.
{ "id": "/api/v1/code", "name": "Hoody Code", "short_name": "Hoody", "description": "Hoody Code in your browser", "start_url": "/api/v1/code", "scope": "/", "display": "standalone", "background_color": "#1e1e1e", "theme_color": "#1e1e1e", "icons": [ { "src": "/favicon.ico", "sizes": "any", "type": "image/x-icon", "purpose": "any" } ]}curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/api/v1/code/manifest.json" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.code.ui.getManifest();Static assets
Section titled “Static assets”GET /favicon.ico
Section titled “GET /favicon.ico”The configured icon, or 204 when none is configured. Referenced by the web application manifest.
This endpoint takes no parameters.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”The icon bytes. Served as image/x-icon or image/png depending on what is configured.
No icon is configured for this origin.
curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/favicon.ico" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.code.ui.getFavicon();GET /robots.txt
Section titled “GET /robots.txt”Crawler policy. Disallows crawling. Container URLs are reachable by anyone holding the identifiers in the hostname, so they should not be indexed.
This endpoint takes no parameters.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”User-agent: *Disallow: /curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/robots.txt" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.code.ui.getRobots();GET /security.txt
Section titled “GET /security.txt”Security contact details, per RFC 9116. Also served at /.well-known/security.txt.
This endpoint takes no parameters.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”Contact: mailto:security@hoody.aiExpires: 2026-12-31T23:59:59zPreferred-Languages: enNo security policy is bundled with this build.
curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/security.txt" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.code.ui.getSecurityPolicy();GET /.well-known/security.txt
Section titled “GET /.well-known/security.txt”The same document as /security.txt, at the RFC 9116 path.
This endpoint takes no parameters.
Request body
Section titled “Request body”This endpoint accepts no request body.
Response
Section titled “Response”Contact: mailto:security@hoody.aiExpires: 2026-12-31T23:59:59zPreferred-Languages: enNo security policy is bundled with this build.
curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/.well-known/security.txt" \ -H "Authorization: Bearer <token>"