Persistent computer API
Create agents with preloaded Linux sandboxes, run commands, and check status — without opening a chat thread.
The computer API lets you create a Flapjack agent with a persistent Linux computer and drive it directly — execute commands, read status, receive lifecycle webhooks — without sending chat messages. This is the v0 Remote Control surface, designed for platforms like Dassie that spin up an agent per app.
All endpoints require the Authorization: Bearer <api-key-or-jwt> header
and are scoped to the caller's org.
POST /api/agents/from-template
One-call create: agent + persistent sandbox + bootstrap.
Request
{
"name": "Grocery Tracker v2",
"template": "nextjs-fullstack",
"repo": {
"url": "https://github.com/acme/grocery-tracker",
"branch": "main",
"installCmd": "pnpm install"
},
"envVars": [
{ "key": "NODE_ENV", "value": "development" }
],
"sizeClass": "medium",
"webhookUrl": "https://dassie.example.com/api/flapjack/webhook",
"externalAppId": "app_abc123"
}
| Field | Required | Notes |
|---|---|---|
template | yes | node-playwright | python-jupyter | nextjs-fullstack | rust-cargo | blank |
repo.url | no | https://github.com/owner/repo (github.com only in v0) |
envVars[].key | no | POSIX-compatible identifier (^[A-Za-z_][A-Za-z0-9_]*$) |
webhookUrl | no | Public https URL; localhost / private IPs are rejected |
externalAppId | no | Makes the call idempotent — same value → same agent |
sizeClass | no | small | medium (default) | large |
Pass Idempotency-Key: <externalAppId> as a header for extra safety under
concurrent retries.
Response — 201 Created (new)
{
"agent": { "id": "...", "org_id": "...", "name": "..." },
"sandbox": { "agent_id": "...", "status": "bootstrapping" },
"bootstrapRunId": "...",
"template": "nextjs-fullstack"
}
Response — 200 OK (idempotent repeat)
Returns the existing agent with idempotent: true and a sandbox.status
mapped from the latest bootstrap run (ready / error / bootstrapping).
Errors
400 INVALID_TEMPLATE— unknown template id400 INVALID_REPO_URL— nothttps://github.com/owner/repo400 INVALID_WEBHOOK_URL— non-public / IP literal / localhost400 INVALID_ENV_VARS— malformed array or non-POSIX keys400 INVALID_SIZE_CLASS401 UNAUTHORIZED/403 FORBIDDEN— auth / org mismatch
GET /api/agents/{agentId}/computer/status
Aggregate status of the agent's sandbox. Cached server-side for 10 s per agent — poll at up to 6/minute without hitting the exec quota.
{
"sandbox": {
"id": "heyo-abc123",
"status": "ready",
"bootstrapRunId": "...",
"lastUsedAt": "2026-04-13T18:52:00Z"
},
"signals": {
"devServer": { "port": 3000, "listening": true },
"disk": { "used": 1288490188, "total": 10737418240 },
"testsLast": null
}
}
status values: bootstrapping • ready • idle • stopped •
destroyed • error • none.
POST /api/agents/{agentId}/computer/exec
Runs a shell command inside the persistent sandbox. Response is
Server-Sent Events. The SDK wraps it as an AsyncIterable; hit it raw
via curl with -N.
Request
{ "command": "pnpm test", "timeoutSec": 120, "workingDir": "/workspace/app" }
SSE frames
event: exec_started
data: { "execId": "exec_xyz", "sandboxId": "heyo-abc" }
event: stdout
data: { "chunk": "..." }
event: stderr
data: { "chunk": "..." }
event: exit
data: { "ok": true, "exitCode": 0, "durationMs": 4120, "truncated": false }
Commands run via bash -lc so ~/.profile (populated at bootstrap with
your envVars) is sourced.
Rate limits
- 60 exec/min per agent (429 with
Retry-Afteron breach) - 600 exec/min per org ceiling
- 3 concurrent execs per agent
Errors
409 SANDBOX_NOT_READY— no live sandbox; call/from-templatefirst410 SANDBOX_DESTROYED— sandbox gone; rebuild needed429 RATE_LIMITED—reason: "AGENT" | "ORG" | "CONCURRENT"
Webhooks
When a webhookUrl is set on the agent, Flapjack POSTs HMAC-SHA256-signed
JSON to it on lifecycle events.
Events
bootstrap.succeeded/bootstrap.failedcomputer.idled— paused after 2h of inactivitycomputer.destroyedagent.deleted
Payload
{
"event": "bootstrap.succeeded",
"agentId": "...",
"orgId": "...",
"externalAppId": "app_abc123",
"sandboxId": "heyo-abc",
"bootstrapRunId": "...",
"exitCode": 0,
"occurredAt": "2026-04-13T18:54:12.345Z"
}
sandboxId is null when bootstrap.failed fires because sandbox creation itself failed (e.g. the Heyo API returned an error before a sandbox was allocated). Consumers should handle a null sandboxId in their bootstrap.failed handler.
{
"event": "bootstrap.failed",
"agentId": "...",
"orgId": "...",
"externalAppId": "app_abc123",
"sandboxId": null,
"bootstrapRunId": "...",
"exitCode": null,
"occurredAt": "2026-04-13T18:55:03.456Z"
}
Headers
X-Flapjack-Event— event nameX-Flapjack-Signature— hex HMAC-SHA256 of the raw body usingWEBHOOK_SIGNING_SECRET
Verify with the SDK helper:
import { verifyWebhookSignature } from '@maats/flapjack';
const body = await req.text();
const sig = req.headers.get('x-flapjack-signature') ?? '';
if (!(await verifyWebhookSignature(body, sig, process.env.WEBHOOK_SIGNING_SECRET!))) {
return new Response('invalid signature', { status: 401 });
}
Idle stop & resume
A persistent sandbox auto-stops after 2 h of inactivity. The next
/computer/exec call resumes it transparently (status goes idle → waking → ready). This is a cost control — you pay for compute only
while the VM is running.
Audit log
Every exec, status, bootstrap, and delete action writes a row to
sandbox_audit_log with actor_kind (user / api_key), exec_id,
exit_code, and duration_ms. Retention: 90 days.