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
| Path | Auth | What it covers |
|---|---|---|
/v1/me | bearer | Who am I (workspace, role, effective scopes). |
/v1/sandboxes | bearer | Sandbox lifecycle, exec, files, PTY, snapshots, backups, restore. |
/v1/templates | bearer | Template catalog + publish/unpublish/delete. |
/v1/template-builds | bearer | Build templates from Docker images. |
/v1/sandbox-ports | bearer | Public HTTPS routes per sandbox. |
/v1/ssh-access | bearer | SSH bastion tokens per sandbox. |
/v1/volumes | bearer | Persistent storage + attach/detach/search. |
/v1/keys | bearer | Workspace API keys. |
/v1/workspaces | bearer | Workspace metadata + member management. |
/v1/quotas | bearer | Usage, limits, raise-quota requests. |
/v1/billing | bearer | Stripe checkout, portal, subscription state. |
/v1/billing/webhook | Stripe sig | Stripe webhook ingestion. Mounted before auth. |
/v1/spotlight | bearer | Cross-resource search. |
/v1/admin/* | platform-admin | Platform-admin operations (hosts, orders, cells, signups, quotas). |
/v1/public/* | none | Host 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
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Request body / params failed validation. |
| 401 | unauthorized | Missing / wrong API key. |
| 403 | forbidden_scope | Token doesn't carry the scope this route needs. |
| 403 | platform_admin_required | Workspace key on an admin/* route. |
| 404 | not_found | Resource doesn't exist or the token can't see it. |
| 409 | conflict | State precondition failed (e.g. slug collision). |
| 422 | unprocessable | Server-side validation that depends on state. |
| 422 | backup_failed | Backup pipeline rejected (e.g. host loss). |
| 429 | quota_exceeded | Workspace at the limit. Inspect error.data. |
| 429 | rate_limited | Per-key rate limit hit. |
| 500 | internal | Unexpected 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/exec—exit_code,duration_ms,timed_out.GET /v1/sandboxes/:id/files/list—modified_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
| Language | Package | Reference |
|---|---|---|
| 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.