Documentation
API Reference

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"
}
FieldRequiredNotes
templateyesnode-playwright | python-jupyter | nextjs-fullstack | rust-cargo | blank
repo.urlnohttps://github.com/owner/repo (github.com only in v0)
envVars[].keynoPOSIX-compatible identifier (^[A-Za-z_][A-Za-z0-9_]*$)
webhookUrlnoPublic https URL; localhost / private IPs are rejected
externalAppIdnoMakes the call idempotent — same value → same agent
sizeClassnosmall | 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 id
  • 400 INVALID_REPO_URL — not https://github.com/owner/repo
  • 400 INVALID_WEBHOOK_URL — non-public / IP literal / localhost
  • 400 INVALID_ENV_VARS — malformed array or non-POSIX keys
  • 400 INVALID_SIZE_CLASS
  • 401 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: bootstrappingreadyidlestoppeddestroyederrornone.


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-After on breach)
  • 600 exec/min per org ceiling
  • 3 concurrent execs per agent

Errors

  • 409 SANDBOX_NOT_READY — no live sandbox; call /from-template first
  • 410 SANDBOX_DESTROYED — sandbox gone; rebuild needed
  • 429 RATE_LIMITEDreason: "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.failed
  • computer.idled — paused after 2h of inactivity
  • computer.destroyed
  • agent.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 name
  • X-Flapjack-Signature — hex HMAC-SHA256 of the raw body using WEBHOOK_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.

Docs last updated May 11, 2026