API
Create and control a running Composery's real editor terminals from anywhere.
Every Composery serves a small automation API on its own URL, in-process with the
editor, on the same single port. It exposes the editor's persistent terminals to
curl, CI, scripts, and WebSocket clients using API keys minted on the instance.
There is no second execution subsystem: every process created here is a VS Code
terminal running through the pty host.
The API stays at the origin-root /_composery/api/v1/ namespace; it is not under
the browser workbench's /ide/ PWA scope.
This is not a control-plane API. There is no lifecycle here (no create/stop/delete of instances). The instance already exists; this only automates work inside it.
Enabling it
The API is on by default but auto-gated: with no keys, every endpoint returns
401, so it is effectively off until you mint one. To turn it off entirely, set
COMPOSERY_DISABLE_API=true (every endpoint then returns 404).
API keys
Keys are created, listed, and revoked on the instance - being able to open the editor is the authorization. You cannot mint a key remotely without already being able to log in, which is the intended boundary.
In the editor, open the File menu and choose Manage API Keys. It lists the instance's keys and creates new ones; each listed key carries a trash button that revokes it, after a confirmation. A new key's secret appears once, with a button to copy it.
In a terminal, the composery CLI does the same:
composery api key create --name ci # prints the secret ONCE
composery api key list
composery api key revoke <id>The secret (composery_...) is shown once at creation and never again - only its
SHA-256 hash is stored, in <volume>/api/keys.json (0600, on the persistent
volume so keys survive a redeploy; /data by default). Add --json to any command for
machine-readable output.
Authenticate with either header:
Authorization: Bearer composery_...
X-API-Key: composery_...Create a terminal
POST /_composery/api/v1/terminals creates the same persistent pty-backed
terminal as the editor. It returns immediately by default:
curl -X POST https://<your-instance>/_composery/api/v1/terminals \
-H "Authorization: Bearer composery_..." \
-H "Content-Type: application/json" \
-d '{"command":"pnpm dev","cwd":"~/app","title":"dev"}'Set hidden: true to keep it in the editor's background terminal collection.
The default is false; the value is passed to VS Code's hideFromUser launch
config rather than implemented as an API-only visibility state.
For a one-shot, add wait: true. The request then waits for the pty to exit and
returns output, exit_code, timed_out, and truncated:
curl -X POST https://<your-instance>/_composery/api/v1/terminals \
-H "Authorization: Bearer composery_..." \
-H "Content-Type: application/json" \
-d '{"command":"pnpm build","cwd":"~/app","wait":true,"timeout":600}'A pty has one output stream, so output is the raw merged stream. ANSI and
shell-integration sequences are retained. The timeout and output cap apply only
while waiting synchronously.
Disconnecting from a wait stops the terminal, because nothing is left that could
collect the result. Send an Idempotency-Key and it keeps running instead: the
key is what lets a retry pick the result back up, so there is something to come
back to.
/_composery/api/v1/terminalsIn: header
Header Parameters
Makes a retry return the first response instead of creating another
terminal. An in-flight retry waits for the first request. Results
are retained for 5 minutes; a different body with the same key is
rejected with 409.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/_composery/api/v1/terminals" \ -H "Content-Type: application/json" \ -d '{}'{ "output": "string", "exit_code": 0, "timed_out": true, "truncated": true}{ "id": 0, "title": "string", "cwd": "string", "pid": 0, "workspaceId": "string", "running": true}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}Inspect and control terminals
The inventory is every persistent terminal, including the ones you opened in the editor yourself - there is no separate class of API terminal to look for.
/_composery/api/v1/terminalsIn: header
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/_composery/api/v1/terminals"[ { "id": 0, "title": "string", "cwd": "string", "pid": 0, "workspaceId": "string", "running": true }]{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}/_composery/api/v1/terminals/{id}In: header
Path Parameters
1 <= valueResponse Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/_composery/api/v1/terminals/1"{ "id": 0, "title": "string", "cwd": "string", "pid": 0, "workspaceId": "string", "running": true}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}Scrollback is the pty host's own replay: the size events and the raw output it
would send an editor reattaching to that terminal, control sequences included. A
terminal that has never written anything replays as empty rather than as 404.
/_composery/api/v1/terminals/{id}/bufferIn: header
Path Parameters
1 <= valueResponse Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/_composery/api/v1/terminals/1/buffer"{ "events": [ { "cols": 0, "rows": 0, "data": "string" } ], "commands": null}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}Renaming uses VS Code's API title source, the one that survives the process
renaming itself. cols and rows move together, and nothing is applied unless
the whole request is valid.
/_composery/api/v1/terminals/{id}In: header
Path Parameters
1 <= valueRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X PATCH "https://example.com/_composery/api/v1/terminals/1" \ -H "Content-Type: application/json" \ -d '{}'{ "id": 0, "title": "string", "cwd": "string", "pid": 0, "workspaceId": "string", "running": true}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}/_composery/api/v1/terminals/{id}/inputIn: header
Path Parameters
1 <= valueRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/_composery/api/v1/terminals/1/input" \ -H "Content-Type: application/json" \ -d '{ "data": "string" }'{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}/_composery/api/v1/terminals/{id}/signalIn: header
Path Parameters
1 <= valueRequest Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/_composery/api/v1/terminals/1/signal" \ -H "Content-Type: application/json" \ -d '{ "signal": "string" }'{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}/_composery/api/v1/terminals/{id}/clearIn: header
Path Parameters
1 <= valueResponse Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/_composery/api/v1/terminals/1/clear"{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}/_composery/api/v1/terminals/{id}In: header
Path Parameters
1 <= valueResponse Body
application/json
application/json
application/json
application/json
application/json
curl -X DELETE "https://example.com/_composery/api/v1/terminals/1"{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}{ "message": "string"}Attach over WebSocket
WS /_composery/api/v1/terminals/{id} attaches to an existing terminal.
Binary frames from the server are raw terminal output; incoming frames are
terminal input. The attach first replays existing scrollback and then follows
the live tail. On exit, the server sends {"exit":{"code":N}} and closes.
websocat -H "Authorization: Bearer composery_..." \
"wss://<your-instance>/_composery/api/v1/terminals/7"Disconnecting only detaches this stream. The terminal stays in the editor,
running: a command started here keeps going with nobody attached, and you can
pick it up in the editor or attach again later. Closing it in the editor stops
it, the way it stops any terminal, and so does DELETE.
The pty host arbitrates its single flow-control counter across the browser and API attachments, so the same broadcast output is never acknowledged twice, and a terminal nothing is attached to runs at full speed rather than stalling on output no one is there to acknowledge.
Configuration
Timeouts, output caps, and rate limits are overridable through COMPOSERY_API_*
environment variables, listed with their defaults in
Configuration - API.
Webhooks
Webhook senders should call an application route such as /hooks/linear, not the
terminal API directly. The receiver verifies the provider signature against the
unchanged request body, deduplicates the delivery, queues the work, and returns the
provider's success response quickly. Its worker can then run claude -p ...,
codex exec ..., or any other normal command locally. No Composery query parameters
are involved.
For example, run a receiver on 127.0.0.1:3000, add the following to
/etc/caddy/Caddyfile, and give the provider
https://<your-instance>/hooks/linear:
handle /hooks/linear* {
reverse_proxy 127.0.0.1:3000
}The API remains useful when the caller is a system that can set an authorization header. A provider webhook generally cannot, and embedding an API key in a URL would leak it into logs and configuration. The local receiver also has the provider-specific raw-body and retry semantics that a generic command endpoint cannot safely guess.