Platform Guide
Give your own users a sandbox each, without giving them a RemoteHost account
This guide is for building a product on RemoteHost: your users click something in your app, and a sandbox appears for them. They never sign up for RemoteHost, never see it, and never know it is there.
Everything below is the REST API. Your backend holds one API key; your users hold nothing.
Get a key
remote keys create --name production-backendShown once, stored only as a hash. A key acts as the user who created it, is scoped to one org,
and can reach only the routes a platform needs — it cannot create orgs, move billing, invite
people, or mint another key. Give it a lifetime with --expires-in-days, list keys with
remote keys list, and kill one with remote keys revoke <id>.
curl https://api.remotehost.ai/v1/me/orgs -H "Authorization: Bearer $REMOTEHOST_API_KEY"One sandbox per user
Tag every sandbox with the id that user has in your system. That id is the spine of everything else here — filtering, attribution, and the bill you send them.
curl -X POST https://api.remotehost.ai/v1/orgs/$ORG/sandboxes \
-H "Authorization: Bearer $REMOTEHOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"projectId": "'$PROJECT'",
"agent": "claude",
"endUserId": "usr_8f21",
"metadata": { "plan": "pro", "signup_cohort": "2026-09" }
}'endUserId is opaque to us. We never parse it, never join on it, and never assume it is unique —
use your primary key, an email, a hash, whatever you already have. metadata is flat labels
(strings, numbers, booleans; 24 keys) for anything else you want to carry.
Then find their sandboxes without paging your whole org:
curl "https://api.remotehost.ai/v1/orgs/$ORG/sandboxes?endUserId=usr_8f21" \
-H "Authorization: Bearer $REMOTEHOST_API_KEY"One project per customer works too, and a key can create projects
(POST /v1/orgs/{orgId}/projects), so you never have to click through a console to onboard one.
Use projects for hard boundaries — separate repo credentials, separate machine-hour caps — and
endUserId for everything else. A project is the heavier object; you do not want ten thousand.
Show them their app
A sandbox is only useful to your user if they can see what is running inside it. Mint a public preview link for a port:
curl -X POST https://api.remotehost.ai/v1/sandboxes/$SANDBOX/previews \
-H "Authorization: Bearer $REMOTEHOST_API_KEY" \
-H "content-type: application/json" \
-d '{ "port": 3000, "audience": "public", "expiresInSeconds": 3600, "endUserId": "usr_8f21" }'{
"preview": {
"id": "9d1c...",
"audience": "public",
"port": 3000,
"url": "https://3000--sb-8f21.previews.remotehost.ai/?__remotehost_preview=...",
"expiresAt": "2026-09-08T12:00:00.000Z"
}
}Anyone holding that URL can open it. No RemoteHost account, no sign-in, no cookie.
The default is "audience": "team", which is the older behaviour: the URL works only for members
of your org. Nothing becomes public unless you ask for it, and asking requires the
sandbox.preview.share permission — an operator or a viewer can watch a sandbox but cannot
publish one.
Every public link is revocable on its own.
curl -X DELETE https://api.remotehost.ai/v1/sandboxes/$SANDBOX/previews/$LINK \
-H "Authorization: Bearer $REMOTEHOST_API_KEY"That matters more than it sounds. A signed URL with no row behind it cannot be taken back without
invalidating every other URL you have ever issued. Here the token names a link, the link is a
database row, and revoking is an update — one customer's leaked URL is one customer's problem.
Links stop working within seconds across every API instance, expire on their own schedule
(24 hours by default, 30 days maximum), and GET /v1/sandboxes/{id}/previews lists what is
outstanding, including when each was last used.
Run their code
curl -X POST https://api.remotehost.ai/v1/sandboxes/$SANDBOX/exec \
-H "Authorization: Bearer $REMOTEHOST_API_KEY" \
-H "content-type: application/json" \
-d '{ "command": "pnpm install && pnpm dev &", "timeoutSeconds": 120 }'exec waits for the command to exit and returns exitCode, stdout and stderr, so background
anything long-lived. You are not obliged to use the agent that ships in the sandbox: run your own
daemon, expose its port, and drive it from your own infrastructure.
Pay for what runs
The three lifecycle shapes, cheapest last:
| Pattern | How | Cost shape |
|---|---|---|
| Always on | persistencePolicy: "always_on" | Bills continuously |
| Sleep and resume | persistencePolicy: "sleep_resume" | Bills while awake; disk kept |
| Ephemeral | mode: "ephemeral" with ttlSeconds | Bills until the TTL, then gone |
POST /v1/sandboxes/{id}/sleep stops a sandbox and keeps its disk; wake brings it back from
the snapshot. destroy is the end — the disk and its snapshots go with it. Resume on your user's
first interaction and sleep them on idle, and a user who works two hours a week costs two hours.
Ephemeral sandboxes are only allowed on projects marked useCase: "external", which is the
setting that says "these machines are for the people using my product, not for my engineers".
Bill your users
Usage is attributed to the endUserId that was set when the machine ran — denormalised onto each
usage row, so it survives the sandbox being deleted and stays correct if you later reassign one.
curl https://api.remotehost.ai/v1/orgs/$ORG/usage/end-users \
-H "Authorization: Bearer $REMOTEHOST_API_KEY"{
"periodStartsAt": "2026-09-01T00:00:00.000Z",
"endUsers": [
{ "endUserId": "usr_8f21", "machineHours": 12.4, "estimatedCogsUsd": 1.61 },
{ "endUserId": "usr_3b09", "machineHours": 0.8, "estimatedCogsUsd": 0.1 }
]
}That is the whole billing story: our invoice to you is one number, and this is how you split it.
Machine hours still running are included, so the figure moves during the period rather than
jumping at the end. GET /v1/orgs/{orgId}/usage?endUserId=usr_8f21 gives one user against the
plan's limits.
Guardrails
- Concurrency. Your plan caps how many sandboxes run at once;
POST /sandboxesreturns429with the limit in the message when you reach it. Size it before you launch: roughly 10,000 signups tends to mean 100 concurrent sandboxes. - Machine hours. Cap a project with
sandboxMachineHourLimit, or the whole org with usage guardrails, so a runaway loop is a stopped sandbox rather than an invoice. - Rate limit. 600 requests a minute per API key, with
Retry-Afteron the429. - Blast radius. A key is scoped to one org and cannot mint another key, so a leaked key is revoked in one call and cannot have created a second way in.
What we do not have yet
Said plainly, because finding out later is worse:
- No webhooks. Sandbox lifecycle is polled through
GET /v1/sandboxes/{id}, not pushed. - No custom templates. Sandboxes snapshot and resume individually, but you cannot yet build an image once and fork it per user; each sandbox provisions from the standard base.
- Agent is required.
create_sandboxtakesclaudeorcodexeven if you intend to run your own daemon and ignore both.
If any of those decide it for you, tell us — they are the next things on this list.