The desktop, over HTTP.
Create KiCad and FreeCAD environments, see the screen, and send actions from any language. All you need is an API key and an HTTP client.
Base URL: https://www.tryisle.com/api
Authentication
Create an API key in the dashboard, then send it on every API request. Requests are scoped to the key owner. Use the canonical www.tryisle.com host to avoid redirects.
export ISLE_API_KEY='isle_...'
export ISLE_BASE_URL='https://www.tryisle.com'
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes" \
-H "Authorization: Bearer $ISLE_API_KEY"JSON requests use Content-Type: application/json. Responses are JSON unless an endpoint returns PNG or file bytes. Keep API keys in your server or agent process; use your backend for browser integrations. No Python installation is required.
// Server-side JavaScript (Node.js with built-in fetch)
const response = await fetch("https://www.tryisle.com/api/sandboxes", {
headers: { Authorization: `Bearer ${process.env.ISLE_API_KEY}` },
});
const body = await response.json();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${body.error}`);
console.log(body); // Array of sandboxes owned by your accountHTTP quickstart
This shell example uses curl and jq. Choose a new operation key for each environment you intend to create, and reuse that exact key and request body if the create response is lost.
set -eu
export ISLE_CREATE_KEY="desktop-$(date +%s)-$$"
# Save the response so the environment ID survives later steps.
curl --fail-with-body --silent --show-error --max-time 360 \
-X POST "$ISLE_BASE_URL/api/sandboxes" \
-H "Authorization: Bearer $ISLE_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $ISLE_CREATE_KEY" \
-d '{"environment":"kicad","name":"Power supply board","retain":true}' \
-o sandbox.json
SANDBOX_ID=$(jq -er '.id' sandbox.json)
export SANDBOX_ID
printf 'Environment: %s\n' "$SANDBOX_ID"
# A successful create response can still be starting. Poll with a deadline.
ready=false
deadline=$(( $(date +%s) + 120 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
remaining=$(( deadline - $(date +%s) ))
[ "$remaining" -gt 0 ] || break
curl --fail-with-body --silent --show-error --max-time "$remaining" \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID" \
-H "Authorization: Bearer $ISLE_API_KEY" -o sandbox.json
state=$(jq -r '.status' sandbox.json)
case "$state" in
running) ready=true; break ;;
error|stopped) cat sandbox.json; exit 1 ;;
esac
sleep 1
done
if [ "$ready" != true ]; then
printf 'Still starting; inspect sandbox.json and poll again.\n' >&2
exit 1
fi
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/screenshot" \
-H "Authorization: Bearer $ISLE_API_KEY" -o screen.pngThe environment remains running for the following examples. Stop it when you finish to pause compute billing. A client timeout does not cancel provisioning; preserve your operation key and environment ID.
Sandbox lifecycle
| Method and path | Request / response |
|---|---|
| POST /api/sandboxes | JSON: environment (required), name (optional), retain (optional, default true). Requires Idempotency-Key. Returns a sandbox: 201 when newly provisioned, 200 on replay, or 202 when provisioning is pending. |
| GET /api/sandboxes | Returns an array of your sandboxes, newest created first. No pagination parameters. |
| GET /api/sandboxes/{id} | Returns one sandbox. Use this endpoint to poll lifecycle state. |
| PATCH /api/sandboxes/{id} | JSON: {"name":"New project name"}. Returns the updated sandbox. |
| DELETE /api/sandboxes/{id} | Stops and archives the environment. Returns 200 with {"status":"stopping","stop_job_id":"..."}, or {"status":"stopped","already_stopped":true}. No body. |
| POST /api/sandboxes/{id}/resume | Resumes a stopped environment. Returns 200 with {"status":"resuming"}; some pending outcomes return 202. No body. Poll until running. |
| DELETE /api/sandboxes/{id}/destroy | Permanently deletes a stopped or failed environment and retained data. Usually returns 202 plus a deletion tracking ID; see Permanent deletion below. |
environment is kicad or freecad. Names are trimmed and limited to 64 characters, with no control characters. On creation, an omitted or blank name uses the application default; renaming requires a nonempty name. retain must be a boolean and controls retained traces and recordings.
Idempotency-Key must contain 1–128 letters, numbers, periods, underscores, colons, or hyphens. Keys are scoped to your account. Reusing a key with different create parameters returns 409. Replaying a deleted environment returns 410 while its idempotency record remains retained. Use a new key only for an intentionally new environment.
| Sandbox fields | Meaning |
|---|---|
| id, name, environment, retain | Environment ID, display name, application, and retention setting. |
| status | starting → running → stopping → stopped; resume moves stopped → resuming → running. Failures can enter error. |
| stream_url | Live desktop viewer URL, or null while unavailable. Use the screenshot endpoint to retrieve image bytes. |
| created_at, started_at, stopped_at | ISO 8601 timestamps; started_at and stopped_at may be null. |
| session_limit_minutes, session_ttl_seconds | The admitted session limit in minutes/seconds, or null when no per-session limit applies. Account usage limits still apply. |
| last_error, last_error_at, recovery_attempts | Latest recorded lifecycle error, its timestamp (nullable), and recovery attempt count. See /errors for history. |
| recording_url, recording_truncated, recording_truncation_reason | Stored recording reference and truncation metadata. Use GET /recording for a downloadable URL; recording_url on the sandbox is not a download URL. |
Creation may also return provisioning_pending and provisioning_requires_manual_intervention. Treat HTTP 202 as an accepted operation that still needs polling. Check status before sending input or files; those endpoints require running.
# Stop. Repeating this request while shutdown runs is safe.
curl --fail-with-body --silent --show-error -X DELETE \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID" \
-H "Authorization: Bearer $ISLE_API_KEY"
# Poll GET /api/sandboxes/$SANDBOX_ID until status is "stopped".
# Once stopped, resume when you want to continue:
curl --fail-with-body --silent --show-error -X POST \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/resume" \
-H "Authorization: Bearer $ISLE_API_KEY"
# Poll GET again until status is "running" before controlling the desktop.Stop completion is asynchronous even when the HTTP status is 200. Resume can return resume_outcome: "unknown" with retry_safe: false; poll the environment instead of immediately resending resume.
Screen and input
All three endpoints require a running environment. GET /api/sandboxes/{id}/screenshot returns raw image/png bytes, with private, noncached responses.
| Endpoint | JSON body |
|---|---|
| POST /api/sandboxes/{id}/mouse | action: "move", "click", "double_click", or "scroll"; required integer x and y; optional button: "left" (default), "middle", or "right". |
| POST /api/sandboxes/{id}/keyboard · type | action: "type"; text: a nonempty string of at most 10,000 characters. |
| POST /api/sandboxes/{id}/keyboard · keypress | action: "keypress"; keys: one key, or unique modifiers followed by one key, such as ["CTRL","S"]. |
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/mouse" \
-H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \
-d '{"action":"click","x":500,"y":300,"button":"left"}'
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/keyboard" \
-H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \
-d '{"action":"type","text":"100nF"}'
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/keyboard" \
-H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \
-d '{"action":"keypress","keys":["CTRL","S"]}'Mouse coordinates range from 0 to 100,000; choose coordinates inside your screenshot. For scroll, y is a signed scroll amount (−100,000 to 100,000): positive scrolls down, negative scrolls up. A value of −500 scrolls up five lines; x is the horizontal target and the vertical target is 0. Avoid zero, which currently scrolls up one line.
Keys are case-insensitive: letters, digits, F1–F12, ENTER/RETURN, TAB, ESC/ESCAPE, arrows, SPACE, DELETE, HOME, END, PAGEUP, and PAGEDOWN. Modifiers include CTRL/CONTROL, SHIFT, ALT, OPTION, CMD/COMMAND, and FN. A modifier by itself is invalid.
Successful input returns {"ok":true,"success":true,"effect":"unverifiable","route":"global_input"}, with optional delivery/evidence metadata. This confirms input delivery; take another screenshot to inspect the application result. On an ambiguous 502 or lost response, the action may already have occurred. Do not automatically replay a click or typed text.
File transfer
Use POST /api/sandboxes/{id}/files to upload and GET /api/sandboxes/{id}/files?path=... to download. The environment must be running. Transfers operate on regular files beneath /home/user/work/; paths must be absolute, at most 4,096 characters, and contain no control characters, ./.. segments, or doubled slashes. Symlinks and unsafe workspace boundaries are rejected.
# Multipart upload. path is optional; default: /home/user/work/<filename>.
# Let curl set the multipart Content-Type and boundary.
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/files" \
-H "Authorization: Bearer $ISLE_API_KEY" \
-F '[email protected]_sch' \
-F 'path=/home/user/work/schematic.kicad_sch'
# JSON upload supports utf8 (default) and base64.
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/files" \
-H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \
-d '{"path":"/home/user/work/notes.txt","contents":"Board revision A","encoding":"utf8"}'
# Download raw bytes; URL-encode the remote path.
curl --fail-with-body --silent --show-error --get \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/files" \
-H "Authorization: Bearer $ISLE_API_KEY" \
--data-urlencode 'path=/home/user/work/board.kicad_pcb' \
-o board.kicad_pcbUpload success is {"path":"/home/user/work/notes.txt"}. Downloads return application/octet-stream with an attachment filename. Each decoded file is limited to 4 MiB (4,194,304 bytes). Multipart is preferable for larger uploads: base64 and JSON escaping expand the HTTP body, which must also fit the host's 4.5 MiB request limit.
Checkpoints and recovery
Save in the desktop application before checkpointing. KiCad expects /home/user/work/project.kicad_pro with its saved schematic/PCB and local assets in that workspace. FreeCAD expects /home/user/work/project.FCStd. In-memory edits are not captured.
| Method and path | Request / response |
|---|---|
| GET /api/sandboxes/{id}/checkpoints | Returns live checkpoints while running: an array with id, timestamp (Unix seconds), artifact_hash, safe. Add source=r2 for persisted checkpoints; stopped environments automatically use persisted storage. |
| POST /api/sandboxes/{id}/checkpoints · checkpoint | JSON: {"action":"checkpoint"} (or {}). Returns local checkpoint metadata plus persisted (boolean) and persistence_reason (string or null). |
| POST /api/sandboxes/{id}/checkpoints · recover | JSON: {"action":"recover"}. Restores the latest safe checkpoint and restarts the app; returns {"status":"recovering"}. |
| POST /api/sandboxes/{id}/checkpoints · restart | JSON: {"action":"restart"}. Restarts without restoring a checkpoint; returns {"status":"restarting"}. |
| POST /api/sandboxes/{id}/checkpoints · download | JSON: {"action":"download","checkpoint_id":1}. checkpoint_id must be a positive integer; returns {"url":"..."}, valid for one hour. |
Every POST checkpoint action, including getting a download URL, currently requires a running environment. Persisted listings use checkpointId (not the live list's id), plus lifecycleGeneration, filename, size, safe, sha256, format, and restorable. Use a persisted checkpoint's numeric checkpointId for download.
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/checkpoints" \
-H "Authorization: Bearer $ISLE_API_KEY" -H 'Content-Type: application/json' \
-d '{"action":"checkpoint"}' -o checkpoint.json
# Only persisted=true confirms a durable checkpoint.
jq '{id, safe, persisted, persistence_reason}' checkpoint.json
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/checkpoints?source=r2" \
-H "Authorization: Bearer $ISLE_API_KEY"A successful HTTP response can contain persisted: false; inspect persistence_reason before relying on durable recovery. Reasons include invalid_checkpoint_metadata, checkpoint_not_safe, artifact_changed_after_checkpoint, and storage_failed.
Current KiCad checkpoints are project.kicad.zip bundles (format: "kicad-project-v1", restorable: true) containing the saved workspace, excluding locks, autosaves, UI preferences, backup folders, .git, .cache, and Isle temporary files. The workspace and bundle must each fit within 256 MiB. Older settings-only checkpoints use legacy-kicad-project with restorable: false; they remain downloadable while retained. Recovery can remove regular workspace files added after the checkpoint.
An unsaved project returns HTTP 404 with code: "artifact_not_found" and the expected path. An invalid or changing project returns 422 with code: "artifact_invalid". Save all editors and retry. Inspect GET /api/sandboxes/{id}/monitor after recovery or restart to follow application health.
Events, health, and recordings
| Method and path | Response and parameters |
|---|---|
| GET /api/sandboxes/{id}/events | Retained events, oldest first: [{id, type, data, created_at}]. limit defaults to 200 and is capped at 1,000. after is an exclusive ISO timestamp, not an event ID. Requires retain=true. |
| GET /api/sandboxes/{id}/errors | Lifecycle errors, newest first: [{id, sandbox_id, source, message, details, created_at}]. limit defaults to 100 and is capped at 500. before is an exclusive ISO timestamp. |
| GET /api/sandboxes/{id}/monitor | Running environment only. Includes status, app, checkpoint counts, recovery_count, and last_error / last_error_at. A 200 response with status="monitor_unavailable" means health could not be read. |
| GET /api/sandboxes/{id}/recording | Requires retain=true. Returns {url, truncated, truncation_reason}; url is a signed download link valid for one hour. 404 when no recording is available yet. |
| POST /api/sandboxes/{id}/recording | No body. Confirms an already persisted recording after stop: {key, already_persisted:true, truncated, truncation_reason}. Requires retain=true and stopped. Does not start a recording. |
| GET /api/sandboxes/{id}/export | Downloads a JSON attachment with schema_version:2, session metadata, and steps containing step, timestamp_ms, type, plus details or image_url. Includes recording/screenshot CDN links. retain=false produces an empty steps array and no recording link. |
curl --fail-with-body --silent --show-error --get \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/events" \
-H "Authorization: Bearer $ISLE_API_KEY" \
--data-urlencode 'limit=200' \
--data-urlencode 'after=2026-09-01T00:00:00Z'
# Once stop has finalized a recording:
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/recording" \
-H "Authorization: Bearer $ISLE_API_KEY" -o recording.json
RECORDING_URL=$(jq -er '.url' recording.json)
curl --fail --silent --show-error "$RECORDING_URL" -o recording.mp4Follow signed download URLs directly without forwarding your Isle API key. Request a fresh URL if it expires. Stop finalizes retained recordings; a recording can be truncated at 512 MiB, six hours, or low disk space. Inspect truncated and truncation_reason to distinguish a partial recording.
Event and error endpoints return arrays without a next-page token. Their timestamp filters are exclusive; records sharing a timestamp at a page boundary can be skipped. For a complete retained event trace, use /export, which gathers all events up to the export time.
Permanent deletion
First stop the environment and poll until stopped. DELETE /api/sandboxes/{id}/destroy also accepts error environments. The request deletes retained data and starts compute-provider cleanup; HTTP acceptance is separate from verified erasure.
curl --fail-with-body --silent --show-error -X DELETE \
"$ISLE_BASE_URL/api/sandboxes/$SANDBOX_ID/destroy" \
-H "Authorization: Bearer $ISLE_API_KEY" -o deletion.json
# Tracking may be null when no provider resource ever existed.
TRACKING_ID=$(jq -r '.provider_deletion_tracking_id // empty' deletion.json)
if [ -n "$TRACKING_ID" ]; then
curl --fail-with-body --silent --show-error \
"$ISLE_BASE_URL/api/provider-deletions/$TRACKING_ID" \
-H "Authorization: Bearer $ISLE_API_KEY"
fiThe usual response is 202 with deletion_pending: true, provider_deletion_tracking_id, provider_operation_id, and provider_operation_status. Persist the tracking ID before the sandbox record disappears. Repeated destroy requests can return the prior deletion state.
| Method and path | Response and parameters |
|---|---|
| GET /api/provider-deletions/{trackingId} | Owner-scoped deletion audit: tracking_id, sandbox_id, provider_operation_id, provider_operation_status, provider_http_status, completion/pending/failure flags, requires_manual_verification, isle_data_deleted, error, and timestamps. |
| GET /api/provider-deletions | Returns {deletions, next_cursor, scope}. limit: 1–100 (default 50); cursor: opaque next_cursor from the previous page. scope: active, recent, or all (default). Follow next_cursor until null. |
scope=active includes pending, failed, and manual-verification audits. recent includes completed audits updated within 30 days. all combines active and recent history; it is not an unlimited historical archive.
Poll while provider_deletion_pending is true. Stop polling and inspect the result on provider_deletion_complete, provider_deletion_failed, or requires_manual_verification. The last flag means the resource is absent without a recovered erasure receipt. isle_data_deleted separately reports cleanup of Isle-managed data.
Errors and safe retries
Route errors generally return {"error":"Human-readable message"}. Some include a machine-readable code, path, environment_id, or lifecycle/operation flags. Inspect the HTTP status and operation state together; accepted asynchronous operations may include diagnostic error text.
| HTTP status | Meaning / action |
|---|---|
| 400 | Invalid JSON, unknown JSON fields, invalid parameters, or an operation requiring a running/stopped state. Fix the request or poll lifecycle state. |
| 401 | Missing, invalid, or revoked API key. |
| 403 | Account usage/concurrency limit reached, or file access denied. Inspect error/code. |
| 404 | Resource/file/checkpoint is unavailable or not owned by this account; can also mean no recording yet or an unsaved checkpoint artifact. |
| 409 | Conflicting create parameters, a lifecycle operation still in progress, an unsafe file path, or deletion waiting for data writes. Follow Retry-After when present and inspect retryable/deletion_pending. |
| 410 | The environment associated with a retained creation idempotency key was deleted. |
| 413 | File or HTTP payload is too large. |
| 422 | The path is not a regular file, or the checkpoint artifact is invalid/changing. |
| 500 / 502 / 503 | Server, upstream, or reconciliation failure. Input actions may already have happened; use the retry guidance below. |
Retry a lost create response with the same Idempotency-Key and unchanged parameters. Stop requests are safe to repeat. Poll lifecycle and deletion status with a bounded deadline and backoff; honor Retry-After when supplied. Recheck state before retrying resume, recovery, or other mutations.
{
"error": "Keyboard action outcome could not be verified",
"action_outcome": "unknown",
"retry_safe": false
}On this input error or a transport timeout, take a screenshot and decide whether another action is needed. Automatically retrying a click, keypress, or text entry can repeat an action that already succeeded. Infrastructure-level failures may return a non-JSON body, so retain the HTTP status and handle JSON parsing failures in your client.