froglet.
API reference

REST

HTTP endpoints at https://api.froglet.sh/v1/... Bearer-token auth, JSON envelopes. The CLI and SDK are thin wrappers around this.

The CLI (@kortix/froglet-cli) and SDK (@kortix/froglet-sdk) both speak this same protocol. For end-to-end resource flows (lifecycle, fields, gotchas), the CLI and SDK pages are the canonical reference — they're tested against prod end-to-end and stay in sync with the API surface.

Base URL

https://api.froglet.sh

All routes live under /v1/.... Override with FROGLET_API_URL (CLI) or baseUrl (SDK).

Authentication

Bearer-token. Mint a key on the dashboard's API keys page (it starts with fl_):

Authorization: Bearer fl_...

A key's effective scopes are the intersection of the scopes you requested at creation time and the issuer's current scopes. Demoting the issuer auto-narrows every outstanding key on the next request.

The browser dashboard authenticates with a session cookie instead. The cookie name is set by better-auth; scope to the parent domain so it works across froglet.sh and api.froglet.sh.

Workspace selection

Users with multiple workspaces can target a specific one with:

x-workspace-slug: <workspace-slug>

Without the header, the API picks the user's first workspace. API keys are workspace-scoped at creation — the header is a no-op for key auth.

Response envelope

Success:

{ "data": <payload> }

Failure:

{
  "error": {
    "code": "quota_exceeded",
    "message": "workspace at sandboxes_concurrent limit",
    "data": { "resource": "sandboxes_concurrent", "current": 5, "limit": 5, "requested": 6 }
  }
}

The CLI and SDK unwrap data on success; on failure the CLI exits non-zero with --json printing the body, and the SDK throws FrogletApiError with status, code, message, data.

Top-level routes

PathAuthWhat it covers
/v1/mebearerWho am I (workspace, role, effective scopes).
/v1/sandboxesbearerSandbox lifecycle, exec, files, PTY, snapshots, backups, restore.
/v1/templatesbearerTemplate catalog + publish/unpublish/delete.
/v1/template-buildsbearerBuild templates from Docker images.
/v1/sandbox-portsbearerPublic HTTPS routes per sandbox.
/v1/ssh-accessbearerSSH bastion tokens per sandbox.
/v1/volumesbearerPersistent storage + attach/detach/search.
/v1/keysbearerWorkspace API keys.
/v1/workspacesbearerWorkspace metadata + member management.
/v1/quotasbearerUsage, limits, raise-quota requests.
/v1/billingbearerStripe checkout, portal, subscription state.
/v1/billing/webhookStripe sigStripe webhook ingestion. Mounted before auth.
/v1/spotlightbearerCross-resource search.
/v1/admin/*platform-adminPlatform-admin operations (hosts, orders, cells, signups, quotas).
/v1/public/*noneHost enrollment.

A workspace key on /v1/admin/* returns 403 platform_admin_required. The /v1/public/* branch (host enrollment) is intentionally unauthenticated — guard it via deploy-time network isolation.

Status codes

StatusCodeMeaning
400bad_requestRequest body / params failed validation.
401unauthorizedMissing / wrong API key.
403forbidden_scopeToken doesn't carry the scope this route needs.
403platform_admin_requiredWorkspace key on an admin/* route.
404not_foundResource doesn't exist or the token can't see it.
409conflictState precondition failed (e.g. slug collision).
422unprocessableServer-side validation that depends on state.
422backup_failedBackup pipeline rejected (e.g. host loss).
429quota_exceededWorkspace at the limit. Inspect error.data.
429rate_limitedPer-key rate limit hit.
500internalUnexpected server error.

Casing

Most responses are camelCase. Two known exceptions return snake_case fields and are surfaced as such by the SDK (the CLI hides this):

  • POST /v1/sandboxes/:id/execexit_code, duration_ms, timed_out.
  • GET /v1/sandboxes/:id/files/listmodified_ms.

volumes.create, volumes.attach, volumes.detach take snake_case bodies (size_gb, volume_id, mount_path).

Worked example

curl -s https://api.froglet.sh/v1/me \
  -H "Authorization: Bearer $FROGLET_API_KEY" | jq

# {
#   "data": {
#     "via": "api_key",
#     "workspace": { "id": "...", "slug": "acme" },
#     "role": "owner",
#     "scopes": ["workspace:read", "sandboxes:read", ...]
#   }
# }

Boot a sandbox:

curl -s https://api.froglet.sh/v1/sandboxes \
  -H "Authorization: Bearer $FROGLET_API_KEY" \
  -H "content-type: application/json" \
  -d '{"templateSlug":"ubuntu-24.04","size":"xs","slug":"demo"}' | jq

# {
#   "data": {
#     "id": "fb7e61a0-...",
#     "slug": "demo",
#     "state": "provisioning",
#     ...
#   }
# }

Exec inside it (note snake_case fields in the response):

curl -s https://api.froglet.sh/v1/sandboxes/fb7e61a0-.../exec \
  -H "Authorization: Bearer $FROGLET_API_KEY" \
  -H "content-type: application/json" \
  -d '{"cmd":"uname -a"}' | jq

# {
#   "data": {
#     "stdout": "Linux fb7e61a0-... ...\n",
#     "stderr": "",
#     "exit_code": 0,
#     "duration_ms": 6,
#     "timed_out": false
#   }
# }

Streaming endpoints

Build logs are served as Server-Sent Events. Get the URL from template-builds, then hit it with curl -N:

url=$(froglet builds log-url <build-id>)
curl -N -H "Authorization: Bearer $FROGLET_API_KEY" "$url"

A sandbox PTY is a WebSocket. Mint a 60-second-TTL ticket and connect:

GET /v1/sandboxes/:id/pty-ticket  →  { url: "wss://...?ticket=<jwt>" }

Rate limits

Per-key. On limit hit, the API returns 429 rate_limited. Use exponential backoff with jitter — the CLI does this automatically.

Versioning

Routes are versioned under /v1/. Field renames are uncommon but happen — the CLI and SDK guard against them with versioned response schemas. Pin a CLI/SDK version in CI to avoid silent breakage on field renames:

npm i -g @kortix/froglet-cli@0.1.0

SDKs

LanguagePackageReference
TypeScript@kortix/froglet-sdk/docs/sdk/typescript
CLI (Rust)@kortix/froglet-cli/docs/cli

Both ship one-for-one with the API surface — if there's a CLI command or an SDK method for it, the REST route exists. For curl-only workflows, the CLI page is the most useful reference: each command lists the verb and path it shells to via --json.