Instance Management API
Section titled “Instance Management API”The Instance Management API lets you stage VS Code extensions from a remote URL and inspect what an instance has staged versus what it appears to have installed. These endpoints are part of the code service that runs inside a Hoody container, and they target it at https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com.
Staging and installing are not the same thing. The editor installs staged extensions only while an instance is starting, so a successful stage reaches a running instance at its next start, not the one that returned the response. The install response reports this as appliesAt and requiresRestartToApply, and the list operation is the only place installation can be confirmed. The stage directory is also shared: an extension staged through one instance is installed by all of them, which the list response reports as desired.scope.
Both operations require an id query parameter naming the instance. There is no discovery branch on these endpoints, so a request that reaches the orchestrator without an id is answered 400 rather than defaulted to a first instance.
List staged and observed extensions
Section titled “List staged and observed extensions”GET /api/v1/code/extensions/list
Section titled “GET /api/v1/code/extensions/list”Reports the staged set (desired) and what the instance appears to have installed (observed) as two separately labelled sections. They disagree between a stage and the next restart, which is normal and is the point of reporting both.
A section that could not be read is reported as status: "unavailable" with a reason, and its extensions key is omitted entirely rather than returned as an empty list: “no extensions” and “could not look” are different answers, and only one of them is a fault. partial is true whenever either section is in that state, so a client can branch on a single field without inspecting both.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | integer | Yes | Which instance this request is about. Required here, unlike on GET /api/v1/code: that operation has a discovery branch to fall back to when no selector is given, this one does not, so a request without an id has named no instance and is rejected rather than defaulted to a first one. The value is read exactly 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. On a code-N service URL the platform’s edge proxy sets it from the hostname, so callers behind the edge neither send it nor can override it; supply it yourself only when addressing the orchestrator directly. |
curl -X GET "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/api/v1/code/extensions/list?id=1" \ -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.extensions.list({ id: 1 });The staged set and the observed set.
{ "instance": { "id": 1, "state": "running", "startedAt": "2026-01-15T10:30:00Z" }, "desired": { "status": "ok", "scope": "shared-across-instances", "extensions": [ { "fileName": "ms-python.python-2026.1.0.vsix", "status": "ok", "reason": null, "scannable": true, "canonicalName": true, "logicalKey": "ms-python.python", "publisher": "ms-python", "name": "python", "version": "2026.1.0", "targetPlatform": null, "sha256": "a1b2c3d4e5f6789012345678901234567890abcdef0123456789abcdef012345", "sizeBytes": 8388608, "stagedAt": "2026-01-15T10:25:00Z" } ] }, "observed": { "status": "ok", "scope": "instance", "instanceId": 1, "extensions": [ { "logicalKey": "ms-python.python", "publisher": "ms-python", "name": "python", "stagedVersion": "2026.1.0", "installedVersion": "2023.1.0", "status": "stale", "reason": "", "installedOnly": false } ] }, "partial": false, "message": "Staged set and observed set may disagree between a stage and the next restart."}The instance selector was unusable.
{ "error": "invalid-selector", "message": "The instance selector could not be parsed."}This service has no extensions directory configured, so there is nowhere to stage and nothing to report. It does NOT mean “no such instance”.
{ "error": "unknown-instance", "message": "This service has no extensions directory configured."}The request could not be completed for a reason on this side.
{ "error": "internal-error", "message": "An unexpected error occurred while listing extensions."}The staged set could not be read. This is state the service owns, so a failure to read it is reported as a fault here rather than as an empty result. The body is the same shape a 200 carries: desired reports status: "unavailable" with the reason, observed is whatever could be read, and partial is true.
{ "instance": { "id": 1, "state": "running", "startedAt": "2026-01-15T10:30:00Z" }, "desired": { "status": "unavailable", "scope": "shared-across-instances", "reason": "stage directory not readable" }, "observed": { "status": "ok", "scope": "instance", "instanceId": 1, "extensions": [] }, "partial": true, "message": "Staged set could not be read; observed set is complete."}Error codes
Section titled “Error codes”The error body is the shared code_ExtensionError shape, which carries a stable error code and a human-readable message. The error code is the field to branch on; the message is written for a person and may be reworded. The set is closed: a value not in this table did not come from this operation.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
invalid-request | Invalid request | The request could not be parsed or failed validation | Fix the request and resend |
invalid-selector | Invalid selector | The instance selector was unusable | Send a valid id query parameter |
unknown-instance | Unknown instance | The named instance does not exist | Use an id of a known instance |
unsupported-media-type | Unsupported media type | The request body was not application/json | Send the body as application/json |
request-too-large | Request too large | The request body exceeded its size limit | Reduce the size of the request body |
destination-refused | Destination refused | The destination URL is not permitted by the outbound policy | Use a URL that satisfies the outbound policy |
downgrade-refused | Downgrade refused | The staged version is newer than the one offered and allowDowngrade was not set | Send allowDowngrade: true or stage a newer version |
extension-too-large | Extension too large | The downloaded archive exceeded its size limit | Stage a smaller extension |
not-a-vsix | Not a VSIX | The download was not a usable VSIX archive | Stage a valid .vsix file |
upstream-error | Upstream error | The far side returned an error response | Retry the request later |
upstream-timeout | Upstream timeout | The download did not finish within the deadline | Retry the request later |
installs-at-capacity | Installs at capacity | No install slot was free | Wait the number of seconds named in Retry-After and retry |
insufficient-storage | Insufficient storage | There is not enough space to store the extension | Free space on the stage directory or stage a smaller extension |
internal-error | Internal error | The request could not be completed for a reason on this side | Retry later or contact support |
Stage a VS Code extension from a URL
Section titled “Stage a VS Code extension from a URL”POST /api/v1/code/extensions/install
Section titled “POST /api/v1/code/extensions/install”Downloads a .vsix archive from a URL and stages it. The editor installs staged extensions only while an instance is starting, so a successful response means the file is in place and the next start of an instance will attempt to install it, not that any running editor now has it. The response says so in appliesAt and requiresRestartToApply, and GET /api/v1/code/extensions/list is the only place installation can be confirmed.
The URL is fetched under the service’s outbound policy: http and https only, no credentials in the URL, and no address inside the container or on a private network, checked again on every redirect. A refused destination answers 403. A far-side failure answers 502 and may be worth retrying.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | integer | Yes | Which instance this request is about. Required here, unlike on GET /api/v1/code: that operation has a discovery branch to fall back to when no selector is given, this one does not, so a request without an id has named no instance and is rejected rather than defaulted to a first one. The value is read exactly 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. On a code-N service URL the platform’s edge proxy sets it from the hostname, so callers behind the edge neither send it nor can override it; supply it yourself only when addressing the orchestrator directly. |
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | — | URL of the .vsix to stage. Must be http or https, without credentials in the URL. |
allowDowngrade | boolean | No | false | Permit replacing a staged version with an older one. Refused by default so a routine restage cannot silently roll an extension back. |
{ "url": "https://example.com/publisher.name-1.2.3.vsix", "allowDowngrade": false}curl -X POST "https://{projectId}-{containerId}-code-1.{server}.containers.hoody.com/api/v1/code/extensions/install?id=1" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/publisher.name-1.2.3.vsix", "allowDowngrade": false }'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.extensions.install( { url: 'https://example.com/publisher.name-1.2.3.vsix', allowDowngrade: false, }, { id: 1 },);Staged. outcome distinguishes a first stage, a replacement, and a restage of bytes already present.
{ "instanceId": 1, "outcome": "staged", "requiresRestartToApply": true, "appliesAt": "next-instance-start", "extension": { "logicalKey": "ms-python.python", "publisher": "ms-python", "name": "python", "version": "2026.1.0", "targetPlatform": null, "sha256": "a1b2c3d4e5f6789012345678901234567890abcdef0123456789abcdef012345", "sizeBytes": 8388608 }, "replaced": null, "removedDuplicates": [], "stageScope": "shared-across-instances", "message": "Staged for installation at the next instance start."}The request body, the URL, or the instance selector was unusable.
{ "error": "invalid-request", "message": "The request body could not be parsed."}The destination is not permitted. Retrying the same URL will not change this on its own, and nothing in the request can widen the policy. It is not a guarantee about later: a name’s address, and a redirect’s target, can both change.
{ "error": "destination-refused", "message": "The destination URL is not permitted by the outbound policy."}This service has no extensions directory configured, so there is nowhere to stage and nothing to report. It does NOT mean “no such instance”.
{ "error": "unknown-instance", "message": "This service has no extensions directory configured."}The staged version is newer than the one offered and no downgrade was requested. Send allowDowngrade to replace it anyway. Concurrent stages of the same extension do NOT produce this: they are serialised and each one gets its own answer.
{ "error": "downgrade-refused", "message": "The staged version is newer than the one offered."}The request body or the downloaded archive exceeded its limit.
{ "error": "request-too-large", "message": "The request body or archive exceeded its size limit."}The request body was not application/json.
{ "error": "unsupported-media-type", "message": "The request body must be application/json."}The download was not a usable VSIX.
{ "error": "not-a-vsix", "message": "The downloaded archive is not a valid VSIX."}The stage could not be completed for a reason on this side, such as a failure writing to the store.
{ "error": "internal-error", "message": "The stage could not be completed."}The far side failed, or redirected somewhere this service will not follow. Retrying may succeed.
{ "error": "upstream-error", "message": "The far side returned an error or a redirect this service will not follow."}No install slot was free. An install holds the whole archive in memory until the store has written it, so a limited number run at once; this request was refused before anything was downloaded, so no partial work was started and nothing was staged. Not a statement about the caller: the same answer is given whether the slot is held by this client or another one. Retry-After names the seconds to wait, and the body repeats it as retryAfterSeconds. The error code is installs-at-capacity.
# Response includes a Retry-After header in addition to the JSON body.{ "error": "installs-at-capacity", "message": "No install slot was free. Retry after the number of seconds named in Retry-After.", "retryAfterSeconds": 5}The download did not finish within the deadline.
{ "error": "upstream-timeout", "message": "The download did not finish within the deadline."}There is not enough space to store the extension.
{ "error": "insufficient-storage", "message": "There is not enough space to store the extension."}Error codes
Section titled “Error codes”The error body is the shared code_ExtensionError shape, which carries a stable error code and a human-readable message. The error code is the field to branch on; the message is written for a person and may be reworded. The set is closed: a value not in this table did not come from this operation.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
invalid-request | Invalid request | The request body could not be parsed or failed validation | Fix the request body and resend |
invalid-selector | Invalid selector | The instance selector was unusable | Send a valid id query parameter |
unknown-instance | Unknown instance | The named instance does not exist | Use an id of a known instance |
unsupported-media-type | Unsupported media type | The request body was not application/json | Send the body as application/json |
request-too-large | Request too large | The request body exceeded its size limit | Reduce the size of the request body |
destination-refused | Destination refused | The destination URL is not permitted by the outbound policy | Use a URL that satisfies the outbound policy |
downgrade-refused | Downgrade refused | The staged version is newer than the one offered and allowDowngrade was not set | Send allowDowngrade: true or stage a newer version |
extension-too-large | Extension too large | The downloaded archive exceeded its size limit | Stage a smaller extension |
not-a-vsix | Not a VSIX | The download was not a usable VSIX archive | Stage a valid .vsix file |
upstream-error | Upstream error | The far side returned an error response | Retry the request later |
upstream-timeout | Upstream timeout | The download did not finish within the deadline | Retry the request later |
installs-at-capacity | Installs at capacity | No install slot was free | Wait the number of seconds named in Retry-After and retry |
insufficient-storage | Insufficient storage | There is not enough space to store the extension | Free space on the stage directory or stage a smaller extension |
internal-error | Internal error | The stage could not be completed for a reason on this side | Retry later or contact support |