API Reference
The REST API behind the CLI and console
The remote CLI and the console use the same REST API. This reference covers the
public sandbox and platform operations, plus session-only account administration. The CLI defaults to https://api.remotehost.ai;
set REMOTEHOST_API_URL=http://localhost:8787 for local development. The console uses
NEXT_PUBLIC_API_URL.
OpenAPI and SDK
Download the deployed OpenAPI document
for machine-readable request and response schemas. Its server URL includes /v1;
paths in this reference include that prefix explicitly.
The TypeScript SDK exposes sandbox resources and a typed remotehost.raw
client for operations without convenience methods. The contract covers the core
sandbox workflow, project reads and updates, identity, usage, and API-key management.
Console-only administration routes are added incrementally; the document is not an
inventory of every internal or interactive endpoint.
Authentication
Every /v1 request requires an Authorization: Bearer <token> header. Two kinds of token work.
curl https://<api-host>/v1/me/orgs \
-H "Authorization: Bearer $REMOTEHOST_TOKEN"Session tokens are what remote login stores at ~/.config/remotehost/config.json. They carry
the full permissions of the signed-in user and expire after about an hour, so they suit an
interactive client, not an unattended one.
API keys (rh_...) are the credential for scripts, CI, the
MCP server, and building a platform on RemoteHost. Mint one with
remote keys create; it is shown once and stored only as a hash. A key acts as the user who created
it — inheriting exactly their permissions, never more — and is additionally limited to one org
and to a fixed set of routes: it can create and read projects, drive sandboxes, mint and revoke
preview links, and read usage, but cannot create orgs, touch billing, send invitations, connect
agent credentials, delete a project, or mint another key. Those need a session token.
Keys can expire and can be restricted to explicit permission scopes. Their effective
permissions are the intersection of the scopes, the creator's current permissions,
and the route allow-list. Omitting scopes (or passing null) leaves the key unscoped;
[] grants no permissions. A key always belongs to one organization.
Keys are rate limited to 600 requests a minute per API instance, with a token bucket
that refills continuously. This is a runaway-request guard, not a global quota. Over the
limit, the API returns 429 with a Retry-After header. Sandbox starts also have
separate plan limits.
Create a key
A signed-in user with api_keys.manage can call:
curl https://api.remotehost.ai/v1/orgs/$ORG_ID/api-keys \
-H "Authorization: Bearer $REMOTEHOST_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"usage reporter","expiresInDays":30,"scopes":["billing.read"]}'The 201 response contains { "apiKey": { ... }, "token": "rh_..." }. Save the token
securely; it cannot be retrieved again. expiresInDays is an integer from 1 to 365;
omitted or null means no expiry. GET .../api-keys returns metadata, including scopes,
expiry and revocation timestamps. DELETE .../api-keys/:keyId revokes a key. All three
operations require a user session; an API key cannot manage other keys.
Caller identity
GET /v1/me returns { "id": "...", "email": "...", "displayName": "..." }.
Email and display name may be null. With an API key, this is the key creator's identity.
Health check
GET /healthUnauthenticated. Returns { "ok": true, "service": "remotehost-api" }.
Orgs
| Method & path | Description |
|---|---|
GET /v1/me/orgs | List orgs the caller belongs to |
POST /v1/orgs | Create an org |
GET /v1/orgs/:orgId/members | List org members |
GET /v1/orgs/:orgId/subscription | Current plan and status |
PATCH /v1/orgs/:orgId/subscription/usage-guardrails | Set sandbox usage cap and overage policy |
GET /v1/orgs/:orgId/usage | Machine-hour usage against allowance and hard cap |
GET /v1/orgs/:orgId/usage/end-users | Machine hours grouped by your own end users |
GET /v1/orgs/:orgId/usage?projectId=:projectId | Project-scoped machine-hour usage against project cap |
GET /v1/orgs/:orgId/activity | Org activity feed (lifecycle events) |
GET /v1/orgs/:orgId/api-keys | List org API keys |
POST /v1/orgs/:orgId/api-keys | Mint an API key (returns the token once) |
DELETE /v1/orgs/:orgId/api-keys/:keyId | Revoke an API key |
POST /v1/orgs/:orgId/billing/checkout | Start a Stripe checkout session for a plan |
Usage guardrails
PATCH /v1/orgs/:orgId/subscription/usage-guardrailsOrg admins can set a hard sandbox machine-hour cap and whether overage is allowed.
{
"sandboxMachineHourLimit": 250,
"sandboxOverageEnabled": false
}For project-level blast-radius control, org admins can set a project cap:
PATCH /v1/orgs/:orgId/projects/:projectId/usage-guardrails{
"sandboxMachineHourLimit": 50
}Set sandboxMachineHourLimit to null to remove the project cap. A project
cap is enforced inside the org cap and uses the same billing period as the
subscription. Add ?projectId=:projectId to the usage endpoint to return
project-scoped usage and alert state.
Set sandboxMachineHourLimit to null to fall back to the catalog included
allowance when overage is disabled. When overage is enabled and no explicit cap
is set, usage may continue without a catalog cap for custom contracts.
The usage response exposes both the effective cap and the explicit override.
sandboxMachineHourLimit is the cap enforced for the current period, while
sandboxMachineHourLimitOverride is null when the subscription is using its
default included allowance.
{
"includedMachineHours": 200,
"sandboxMachineHourLimit": 200,
"sandboxMachineHourLimitOverride": null,
"sandboxOverageEnabled": false,
"usedMachineHours": 12.5,
"remainingMachineHours": 187.5,
"usageAlert": {
"percentUsed": 6.25,
"status": "normal",
"message": "Sandbox machine-hour usage is within the current cap."
}
}Usage attribution
GET /v1/orgs/:orgId/usage accepts optional projectId and endUserId filters.
It returns the billing period, used and remaining machine hours, effective caps,
overage estimates, and usageAlert. Uncapped quantities may be null.
GET /v1/orgs/:orgId/usage/end-users returns { "periodStartsAt": "...", "periodEndsAt": "...", "endUsers": [...] }. Each entry contains endUserId,
machineHours, and estimatedCogsUsd, sorted by machine hours descending. Usage
without an end-user ID is excluded from this breakdown. Both routes require
billing.read and return 402 when there is no active billing period.
Invitations
| Method & path | Description |
|---|---|
GET /v1/orgs/:orgId/invitations | List pending invitations |
POST /v1/orgs/:orgId/invitations | Invite a member by email |
DELETE /v1/orgs/:orgId/invitations/:invitationId | Revoke a pending invitation |
GET /v1/invitations/:token | Look up an invitation by its token |
POST /v1/invitations/:token/accept | Accept an invitation as the authenticated user |
Projects
| Method & path | Description |
|---|---|
GET /v1/orgs/:orgId/projects | List projects |
POST /v1/orgs/:orgId/projects | Create a project |
PATCH /v1/orgs/:orgId/projects/:projectId | Update project metadata |
PATCH /v1/orgs/:orgId/projects/:projectId/usage-guardrails | Set project sandbox cap |
Sandboxes
| Method & path | Description |
|---|---|
GET /v1/orgs/:orgId/sandboxes | List sandboxes (excludes deleted) |
POST /v1/orgs/:orgId/sandboxes | Create and provision a sandbox |
GET /v1/sandboxes/:sandboxId | Fetch one sandbox by id |
POST /v1/sandboxes/:sandboxId/exec | Run a shell command in the sandbox and wait for it |
POST /v1/sandboxes/:sandboxId/previews | Mint a preview URL for a port (team or public) |
GET /v1/sandboxes/:sandboxId/previews | List the sandbox's public preview links |
DELETE /v1/sandboxes/:sandboxId/previews/:id | Revoke a preview link |
POST /v1/sandboxes/:sandboxId/sleep | Put a running sandbox to sleep |
POST /v1/sandboxes/:sandboxId/wake | Wake a sleeping sandbox from its snapshot |
POST /v1/sandboxes/:sandboxId/resize | Set an exact sandbox allocation |
POST /v1/sandboxes/:sandboxId/destroy | Permanently destroy a sandbox and its snapshot |
POST /v1/sandboxes/:sandboxId/renew | Extend an ephemeral sandbox's TTL within its session cap |
GET /v1/sandboxes/:sandboxId/files | List a directory |
GET /v1/sandboxes/:sandboxId/files/snapshot | Read a directory tree |
GET /v1/sandboxes/:sandboxId/file | Read a file |
PUT /v1/sandboxes/:sandboxId/file | Write a file |
GET /v1/sandboxes/:sandboxId/metrics/live | Read resource metrics |
POST /v1/sandboxes/:sandboxId/metrics/sample | Sample Firecracker sandbox CPU, memory, and disk pressure |
Create a sandbox
POST /v1/orgs/:orgId/sandboxes{
"agent": "claude",
"projectId": "proj_123",
"name": "my-sandbox",
"repository": "org/repo",
"branch": "main",
"profile": "auto",
"sshPublicKey": "ssh-ed25519 AAAA... remotehost-cli"
}endUserId and metadata attribute the sandbox to one of your users — see the
Platform Guide. agent and projectId are required (agent is "claude" or "codex") — every other field
falls back to the project's defaults, a selected sandbox profile, or a server-generated value.
profile can be "auto", "agent-small", "agent-standard", "build-heavy",
"browser-heavy", "repo-large", or "long-running". If machineSize is also provided, it
overrides the profile's initial machine tier. Without either field, profile is inferred from
the requested machine size for backward compatibility. Region and infrastructure tier are chosen
server-side from the org's plan and are not client inputs. Returns 201 with
{ "sandbox": { ... } }; the sandbox payload includes sandbox_profile so clients can display
and meter the selected workload profile, plus allocated_vcpu, allocated_memory_gb, and
allocated_disk_gb for the concrete resource shape. Returns 400 if projectId is missing or
unknown, or 409 if the org is at its concurrent-sandbox limit.
Preview URLs
POST /v1/sandboxes/:sandboxId/previews{
"port": 3000,
"audience": "public",
"expiresInSeconds": 3600,
"endUserId": "usr_8f21",
"label": "customer demo"
}audience defaults to "team": the URL resolves only for a signed-in member of the sandbox's
org, and no row is stored. "public" mints a revocable link anyone holding the URL can open —
it requires the sandbox.preview.share permission, expires after expiresInSeconds (24 hours
by default, 30 days maximum), and returns the link's id for revoking later. See the
Platform Guide.
List sandboxes
GET /v1/orgs/:orgId/sandboxesReturns { "sandboxes": [...] }, newest first, excluding sandboxes with status: "deleted".
Sleep a sandbox
POST /v1/sandboxes/:sandboxId/sleepSnapshots the sandbox, releases its machine, and ends its machine-time billing. The sandbox
reports status stopped while asleep and keeps everything for wake. An ephemeral sandbox has no
snapshot to keep, so this ends it. POST .../stop is a deprecated alias.
Wake a sandbox
POST /v1/sandboxes/:sandboxId/wakeBoots a sleeping sandbox from its own snapshot. Returns 400 if the sandbox is not asleep or has
no snapshot. POST .../resume is a deprecated alias.
Resize a sandbox
POST /v1/sandboxes/:sandboxId/resize{
"vcpu": 4,
"memoryGb": 8,
"diskGb": 80
}Sets an exact allocation target. Sleeping sandboxes update their persisted allocation immediately.
Running sandboxes apply live only when the provider can verify the resource update; otherwise the
requested shape is stored as pendingAllocation and used on the next wake.
Run a command
POST /v1/sandboxes/:sandboxId/exec{
"command": "pnpm test",
"timeoutSeconds": 120
}Runs command through /bin/sh in /code and waits for it to exit. timeoutSeconds defaults
to 120 and caps at 600. Firecracker sandboxes only, and only while the sandbox is running or
ready — wake it first otherwise.
{
"exitCode": 1,
"stdout": "2 failing",
"stderr": "",
"truncated": false,
"timedOut": false
}A non-zero exitCode is a successful request: the command ran and failed. timedOut is true
with exitCode 124 when the timeout was hit. Output over 64 KiB is trimmed from the front and
truncated is set — the tail is kept, since that is where a failing build puts its error.
Sample sandbox metrics
POST /v1/sandboxes/:sandboxId/metrics/sampleSamples CPU, memory, and disk pressure from a running Firecracker sandbox, records a
sandbox_metrics_sample event, and returns the current allocation plus the recommended allocation.
For auto sandboxes, a changed recommendation is stored as pendingAllocation and promoted the
next time the sandbox is provisioned or woken. Live in-place resize still requires an
orchestrator resource-update primitive.
Destroy and renew
POST /v1/sandboxes/:sandboxId/destroy permanently deletes the sandbox and its retained
snapshot. It requires sandbox.destroy and returns { "sandbox": { ... } }.
Use sleep when you intend to resume later.
POST /v1/sandboxes/:sandboxId/renew extends a running ephemeral sandbox's expiry by
its configured TTL. It requires sandbox.stop, an active plan, and an existing TTL.
The plan's session cap still applies; the API returns 410 when that cap is exhausted.
No request body is needed for either operation.
Files
File routes require a running Firecracker sandbox. Reads require sandbox.files.read;
writes require sandbox.files.write. Relative paths resolve beneath /code.
GET .../files?path=/codereturns{ "path": "/code", "truncated": false, "entries": [...] }. Entries havelabel,path, andtype("directory"or"file").GET .../files/snapshot?path=/code&depth=3returns{ "path": "/code", "truncated": false, "directories": { "/code": [...] } }. Depth defaults to 3 and is clamped to 1–5. This is a directory listing, not a persistent backup.GET .../file?path=/code/README.mdreturnspath,content,encoding("utf8"or"base64"), andsizein bytes.pathis required.PUT .../filetakes{ "path": "/code/hello.txt", "content": "hello", "encoding": "utf8" }and returnspathandsize. Encoding defaults to UTF-8. Writes over 2 MiB return413.
Large directory results can be truncated. Check truncated before treating a listing
as complete.
Lifecycle polling and retries
Poll GET /v1/sandboxes/:sandboxId to observe provisioning and lifecycle changes.
The platform does not currently deliver customer lifecycle webhooks. SDK creation
waits for readiness by default; use waitForReady: false to receive the provisioning
response immediately.
The SDK retries safe GET/HEAD requests on transient failures, respecting Retry-After.
It does not automatically retry writes. If a create request loses its response, inspect
the sandbox list before submitting another create: a timeout does not prove that the
first operation failed.
Errors
Error responses are { "error": { "message": "<message>" } } with a matching HTTP status code
— 400 for invalid input, 401/403 for auth and membership failures, 402 for billing
issues, 404 for missing resources, 409 for plan/limit conflicts, 429 for rate limits,
500 for provisioning or database failures.