Remotehost Docs

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 /health

Unauthenticated. Returns { "ok": true, "service": "remotehost-api" }.

Orgs

Method & pathDescription
GET /v1/me/orgsList orgs the caller belongs to
POST /v1/orgsCreate an org
GET /v1/orgs/:orgId/membersList org members
GET /v1/orgs/:orgId/subscriptionCurrent plan and status
PATCH /v1/orgs/:orgId/subscription/usage-guardrailsSet sandbox usage cap and overage policy
GET /v1/orgs/:orgId/usageMachine-hour usage against allowance and hard cap
GET /v1/orgs/:orgId/usage/end-usersMachine hours grouped by your own end users
GET /v1/orgs/:orgId/usage?projectId=:projectIdProject-scoped machine-hour usage against project cap
GET /v1/orgs/:orgId/activityOrg activity feed (lifecycle events)
GET /v1/orgs/:orgId/api-keysList org API keys
POST /v1/orgs/:orgId/api-keysMint an API key (returns the token once)
DELETE /v1/orgs/:orgId/api-keys/:keyIdRevoke an API key
POST /v1/orgs/:orgId/billing/checkoutStart a Stripe checkout session for a plan

Usage guardrails

PATCH /v1/orgs/:orgId/subscription/usage-guardrails

Org admins can set a hard sandbox machine-hour cap and whether overage is allowed.

Request body
{
  "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
Request body
{
  "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.

GET /v1/orgs/:orgId/usage
{
  "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 & pathDescription
GET /v1/orgs/:orgId/invitationsList pending invitations
POST /v1/orgs/:orgId/invitationsInvite a member by email
DELETE /v1/orgs/:orgId/invitations/:invitationIdRevoke a pending invitation
GET /v1/invitations/:tokenLook up an invitation by its token
POST /v1/invitations/:token/acceptAccept an invitation as the authenticated user

Projects

Method & pathDescription
GET /v1/orgs/:orgId/projectsList projects
POST /v1/orgs/:orgId/projectsCreate a project
PATCH /v1/orgs/:orgId/projects/:projectIdUpdate project metadata
PATCH /v1/orgs/:orgId/projects/:projectId/usage-guardrailsSet project sandbox cap

Sandboxes

Method & pathDescription
GET /v1/orgs/:orgId/sandboxesList sandboxes (excludes deleted)
POST /v1/orgs/:orgId/sandboxesCreate and provision a sandbox
GET /v1/sandboxes/:sandboxIdFetch one sandbox by id
POST /v1/sandboxes/:sandboxId/execRun a shell command in the sandbox and wait for it
POST /v1/sandboxes/:sandboxId/previewsMint a preview URL for a port (team or public)
GET /v1/sandboxes/:sandboxId/previewsList the sandbox's public preview links
DELETE /v1/sandboxes/:sandboxId/previews/:idRevoke a preview link
POST /v1/sandboxes/:sandboxId/sleepPut a running sandbox to sleep
POST /v1/sandboxes/:sandboxId/wakeWake a sleeping sandbox from its snapshot
POST /v1/sandboxes/:sandboxId/resizeSet an exact sandbox allocation
POST /v1/sandboxes/:sandboxId/destroyPermanently destroy a sandbox and its snapshot
POST /v1/sandboxes/:sandboxId/renewExtend an ephemeral sandbox's TTL within its session cap
GET /v1/sandboxes/:sandboxId/filesList a directory
GET /v1/sandboxes/:sandboxId/files/snapshotRead a directory tree
GET /v1/sandboxes/:sandboxId/fileRead a file
PUT /v1/sandboxes/:sandboxId/fileWrite a file
GET /v1/sandboxes/:sandboxId/metrics/liveRead resource metrics
POST /v1/sandboxes/:sandboxId/metrics/sampleSample Firecracker sandbox CPU, memory, and disk pressure

Create a sandbox

POST /v1/orgs/:orgId/sandboxes
Request body
{
  "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
Request body
{
  "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/sandboxes

Returns { "sandboxes": [...] }, newest first, excluding sandboxes with status: "deleted".

Sleep a sandbox

POST /v1/sandboxes/:sandboxId/sleep

Snapshots 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/wake

Boots 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
Request body
{
  "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
Request body
{
  "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.

Response
{
  "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/sample

Samples 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=/code returns { "path": "/code", "truncated": false, "entries": [...] }. Entries have label, path, and type ("directory" or "file").
  • GET .../files/snapshot?path=/code&depth=3 returns { "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.md returns path, content, encoding ("utf8" or "base64"), and size in bytes. path is required.
  • PUT .../file takes { "path": "/code/hello.txt", "content": "hello", "encoding": "utf8" } and returns path and size. Encoding defaults to UTF-8. Writes over 2 MiB return 413.

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.

On this page