Computer Use APIv1

Give your code
eyes and hands.

Send a screenshot. Get structured mouse and keyboard actions back. One REST endpoint — for automation, browser testing, and AI agents that interact with any GUI.

3.5s
median step latency
10
action primitives
99.9%
uptime SLA
One call. Four lines.

Built for any stack.

Pure REST. No SDK lock-in, no extra servers, no browser drivers.

POST /v1/predict
1import requests, base64
2 
3img = base64.b64encode(open("screen.png", "rb").read()).decode()
4 
5r = requests.post(
6 "https://coasty.ai/v1/predict",
7 headers={"X-API-Key": "sk-coasty-live-..."},
8 json={
9 "screenshot": img,
10 "instruction": "Click the search bar and type 'hello'",
11 },
12)
13 
14for a in r.json()["actions"]:
15 print(a["action_type"], a["params"])
Returns a stream of typed actions — coordinates, keystrokes, and confidence.
Machines API

Real desktops. Real shells. Real automation.

Provision a sandbox or production VM, then drive it with actions, terminal commands, browser automation, or file ops. One auth header, fifteen endpoints.

POST /v1/machines + POST /v1/machines/{id}/actions
1import requests
2 
3# Provision a sandbox VM (sk-coasty-test-* — instant, no AWS, no billing).
4r = requests.post(
5 "https://coasty.ai/v1/machines",
6 headers={
7 "X-API-Key": "sk-coasty-test-...",
8 "Idempotency-Key": "demo-001",
9 },
10 json={
11 "display_name": "automation-bot",
12 "os_type": "linux",
13 "desktop_enabled": True,
14 },
15)
16machine = r.json()["machine"]
17 
18# Drive it: click at (512, 340)
19requests.post(
20 f"https://coasty.ai/v1/machines/{machine['id']}/actions",
21 headers={"X-API-Key": "sk-coasty-test-..."},
22 json={"command": "click", "parameters": {"x": 512, "y": 340}},
23)
Sandbox keys (sk-coasty-test-*) return a mock VM in < 50 ms — instant retries, zero AWS cost.
POST /v1/machinesGET /v1/machinesGET /v1/machines/pricingPATCH /v1/machines/{id}DELETE /v1/machines/{id}POST /v1/machines/{id}/startPOST /v1/machines/{id}/stopPOST /v1/machines/{id}/restartPOST /v1/machines/{id}/snapshotGET /v1/machines/{id}/screenshotGET /v1/machines/{id}/connectionPOST /v1/machines/{id}/actionsPOST /v1/machines/{id}/actions/batchPOST /v1/machines/{id}/browser/{op}POST /v1/machines/{id}/terminalPOST /v1/machines/{id}/files/{op}
Schedules API

Cron, webhooks, email, chains.

Run an agent on a cron, fire it from any webhook with HMAC, or chain schedules together. Webhook fires free (no routing fee, $0.20 wallet gate) · execution 10 cr/min from subscription credits (min 20, 6 h cap) · sandbox is free.

POST /v1/schedules  +  POST /triggers (webhook)  +  sign & fire
1import requests, hmac, hashlib, time
2 
3# 1. Create a daily 9 AM ET schedule
4sched = requests.post(
5 "https://coasty.ai/v1/schedules",
6 headers={"X-API-Key": "sk-coasty-test-..."},
7 json={
8 "name": "morning briefing",
9 "machine_id": "550e8400-e29b-41d4-a716-446655440000",
10 "task_prompt": "Summarize unread Gmail and post to Slack.",
11 "frequency": "daily",
12 "time": "09:00",
13 "timezone": "America/New_York",
14 },
15).json()
16 
17# 2. Add a webhook trigger — store the secret immediately
18trigger = requests.post(
19 f"https://coasty.ai/v1/schedules/{sched['id']}/triggers",
20 headers={"X-API-Key": "sk-coasty-test-..."},
21 json={"kind": "webhook"},
22).json()
23secret = trigger["webhook_secret"] # whsec_<64 hex> — store this
24 
25# 3. Sign + fire the webhook from any external system
26ts = int(time.time())
27body = b'{"event":"order.placed"}'
28sig = hmac.new(secret.encode(), f"{ts}.".encode() + body,
29 hashlib.sha256).hexdigest()
30requests.post(
31 trigger["webhook_url"],
32 headers={"Coasty-Signature": f"t={ts},v1={sig}"},
33 data=body,
34)
Schedules created via API show up in your /schedules dashboard automatically — same user_id, same view.
POST /v1/schedulesGET /v1/schedulesPATCH /v1/schedules/{id}DELETE /v1/schedules/{id}POST /v1/schedules/{id}/runPOST /v1/schedules/{id}/pausePOST /v1/schedules/{id}/resumeGET /v1/schedules/{id}/runsGET /v1/schedules/{id}/runs/{run_id}POST /v1/schedules/{id}/triggersDELETE /v1/schedules/{id}/triggers/{tid}POST /v1/triggers/webhook/{wh} ← unauth · HMACPOST /v1/triggers/email-mailbox
Local automation

Automate any screen. Yours included.

predict, ground and sessions are screen-agnostic — feed them screenshots from your own desktop, a Playwright page, a phone emulator, or a VNC frame, and execute the returned actions with pyautogui, page.mouse, or adb. No VM required.

your screen → /v1 → your input events
1# Automate YOUR screen — no VM needed. pip install requests mss pyautogui pillow
2import base64, io, time, uuid, requests, mss, pyautogui
3from PIL import Image
4 
5API, KEY = "https://coasty.ai/v1", "sk-coasty-test-..." # test key = free while you build
6HDRS = {"X-API-Key": KEY}
7pyautogui.FAILSAFE = True # slam the mouse into a corner to abort instantly
8 
9REAL_W, REAL_H = pyautogui.size() # your actual desktop resolution
10SEND_W, SEND_H = 1280, 720 # what we tell the model (SD = 1 credit cheaper)
11SX, SY = REAL_W / SEND_W, REAL_H / SEND_H # scale model coords -> real pixels
12 
13def screenshot_b64():
14 with mss.mss() as sct:
15 shot = sct.grab(sct.monitors[1]) # primary monitor
16 img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
17 img = img.resize((SEND_W, SEND_H)) # MUST match screen_width/height
18 buf = io.BytesIO(); img.save(buf, format="PNG")
19 return base64.b64encode(buf.getvalue()).decode()
20 
21def execute(a):
22 t, p = a["action_type"], a["params"]
23 if t == "click":
24 pyautogui.click(p["x"] * SX, p["y"] * SY,
25 clicks=p.get("clicks", 1), button=p.get("button", "left"))
26 elif t == "type_text": pyautogui.write(p["text"], interval=0.02)
27 elif t == "key_press": pyautogui.press(p["keys"])
28 elif t == "key_combo": pyautogui.hotkey(*p["keys"])
29 elif t == "scroll": pyautogui.scroll(p["clicks"]) # +up / -down, like pyautogui
30 elif t == "drag":
31 pyautogui.moveTo(p["x1"] * SX, p["y1"] * SY)
32 pyautogui.dragTo(p["x2"] * SX, p["y2"] * SY, duration=0.4)
33 elif t == "wait": time.sleep(p["seconds"])
34 return t
35 
36# A session keeps screenshot history so the model remembers what it already did.
37sess = requests.post(f"{API}/sessions", headers=HDRS, json={
38 "cua_version": "v3",
39 "screen_width": SEND_W, "screen_height": SEND_H,
40 # Optional best-practice steering (Starter+). Pick a preset from the docs:
41 "instructions": "Click the visual center of elements. If the target is not visible, scroll toward it, never guess.",
42}).json()
43sid = sess["session_id"]
44 
45task = "Open the calculator and compute 42 * 17"
46try:
47 for step in range(25):
48 r = requests.post(
49 f"{API}/sessions/{sid}/predict",
50 headers={**HDRS, "Idempotency-Key": f"step-{sid}-{step}-{uuid.uuid4().hex[:8]}"},
51 json={"screenshot": screenshot_b64(), "instruction": task},
52 timeout=120,
53 ).json()
54 print(f"step {r['step']}: {r['reasoning'][:80]}")
55 for a in r["actions"]:
56 if execute(a) in ("done", "fail"):
57 raise SystemExit(f"finished: {r['status']} — {r['reasoning']}")
58 time.sleep(0.5) # let the UI settle
59finally:
60 requests.delete(f"{API}/sessions/{sid}", headers=HDRS) # stop the session clock
61 
Coordinates come back in the space of the screenshot you sent — scale before you click.
your desktop · mss + pyautoguia browser · Playwrighta phone · adb screencap + inputVNC / RDP · framebuffer + injected inputa Coasty VM · /v1/machines runs the loop for you

Screenshot in. Actions out.

No selectors. No DOM parsing. No brittle XPath. Just vision.

01

Send screenshot

Base64 PNG/JPEG + plain-language intent

02

AI reasons visually

Vision model identifies the target UI element

03

Execute actions

Typed primitives: click, type, scroll, press…

Vision-First

Works on any UI — web, desktop, mobile, VNC. No DOM access, no selectors, no agents.

Stateful Sessions

Multi-step trajectories. The model remembers what it tried, what worked, and what's next.

Two Engines

V3 for speed (3.5s/step, multi-action). V1 for precision (reflection, single-action).

Any Screen

Browser tabs, desktop apps, mobile emulators, VNC feeds — anything you can capture visually.

10 Action Types

click, double_click, type, scroll, drag, key_press, key_combo, wait, done, fail.

Any Language

Plain REST + JSON. Python, Node, Go, Ruby, PHP, Java, C#, or cURL from your terminal.

Per-request pricing. No subscription.

Billed to your API wallet — 1 credit = $0.01, separate from subscription credits. Charged before execution, refunded on failure. Management endpoints and sandbox keys always free.

EndpointCost
POST /predict5 cr
POST /sessions10 cr
POST /sessions/{id}/predict4 cr
POST /ground3 cr
Run / workflow agent step (v3, v4)5 cr
Run / workflow agent step (v1)8 cr
Machine running — Linux5 cr/hr
Machine running — Windows9 cr/hr
Machine stopped / suspended1 cr/hr
POST /machines/{id}/snapshot1 cr
POST /parseFree
Machine actions · terminal · browser · filesFree
Workflow control-flow stepsFree
Schedules create · run · webhook fireFree
GET /models, /usage, /sessionsFree

Surcharges

Trajectory screenshot+2 cr each
HD image >1280×720+1 cr/image
V1 engine+3 cr/request
System prompt >500 chars+1 cr

Surcharges apply to predict, session predict and ground; session create has none. HD is strictly larger than 1280×720. Gates, not fees: machine provisioning, schedule create, run-now and webhook fires each require a $0.20 API-wallet balance. Scheduled-run execution bills subscription credits at 10 cr/min (min 20 credits, 6 h cap). Machines auto-stop (never destroyed) if the wallet empties. Test keys (sk-coasty-test-*) bill 0 everywhere. Live machine rates: GET /v1/machines/pricing.

Computer Use API

Send a screenshot, get actions back

The CUA API gives your code the ability to see and interact with any screen. Send a screenshot and a natural language instruction — receive structured mouse clicks, keyboard inputs, and scroll commands with exact coordinates.

Authentication

Send your key as an X-API-Key header or Authorization: Bearer. Sign up to create API keys. Charges debit your prepaid developer API wallet (1 credit = 1¢ = $0.01) — separate from your Coasty app subscription, even on Unlimited. Fees are charged before execution and auto-refunded on failure.

header
X-API-Key: sk-coasty-live-your_key_here
# or, equivalently:
Authorization: Bearer sk-coasty-live-your_key_here
  • · Don't paste the literal "Bearer " prefix into an X-API-Key value.
  • · Billed success responses carry X-Credits-Charged + X-Credits-Remaining; the body usage has credits_charged + cost_cents (both 0 on test keys).
  • · Test keys (sk-coasty-test-) never bill and use mock VMs; the wire format matches production.
  • · POST runs / machines / workflows / schedules accept an Idempotency-Key header so retries are safe.

How it Works

1Capture a screenshot of the target screen
2Send it with a natural language instruction
3Receive structured actions (click, type, scroll...)
4Execute the actions in your environment

Quick Start

Choose your language. The predict endpoint is the core of the API — everything else builds on it.

install
pip install requests
predict — single screenshot
import requests, base64

API_KEY = "sk-coasty-live-..."
img = base64.b64encode(open("screen.png", "rb").read()).decode()

r = requests.post(
    "https://coasty.ai/v1/predict",
    headers={"X-API-Key": API_KEY},
    json={
        "screenshot": img,
        "instruction": "Click the search bar and type 'hello'",
    },
)

for action in r.json()["actions"]:
    print(action["action_type"], action["params"])
sessions — multi-step tasks
# Create a session for multi-step tasks
s = requests.post(
    "https://coasty.ai/v1/sessions",
    headers={"X-API-Key": API_KEY},
    json={"cua_version": "v3", "screen_width": 1920, "screen_height": 1080},
).json()

session_id = s["session_id"]

# Send screenshots in a loop
while True:
    screenshot = capture_screenshot()  # your screenshot function
    r = requests.post(
        f"https://coasty.ai/v1/sessions/{session_id}/predict",
        headers={"X-API-Key": API_KEY},
        json={"screenshot": screenshot, "instruction": "Complete the form"},
    ).json()

    for action in r["actions"]:
        execute_action(action)  # your action executor

    if r["status"] in ("done", "fail"):
        break

Response Format

Every prediction returns structured actions with exact coordinates, a status signal, and token usage.

response
{
  "request_id": "req_abc123",
  "actions": [
    {
      "action_type": "click",
      "params": { "x": 512, "y": 340, "button": "left", "clicks": 1 }
    },
    {
      "action_type": "type_text",
      "params": { "text": "hello world" }
    }
  ],
  "reasoning": "I see a search bar at (512, 340)...",
  "status": "continue",
  "usage": {
    "input_tokens": 1523,
    "output_tokens": 245,
    "credits_charged": 5,
    "cost_cents": 5
  }
}

Billed responses also carry X-Credits-Charged + X-Credits-Remaining headers. On a sk-coasty-test- key, credits_charged and cost_cents are both 0.

Action Types

clickMouse click at (x, y)
type_textType a string
key_pressPress a key (enter, tab...)
key_comboCombo (ctrl+c, cmd+v...)
scrollScroll at a position
dragDrag between two points
moveMove cursor
waitPause execution
doneTask completed
failTask impossible

Request Options

Only screenshot and instruction are required.

screenshotstringrequired
instructionstringrequired
cua_version"v3" | "v4" | "v1" (+3 cr)
screen_widthint
screen_heightint
max_actionsint (1-10)
trajectoryarray
system_promptstring
toolsstring[]

Predict Endpoints

Stateless prediction, sessions, and grounding utilities. All require the X-API-Key header.

Prediction
POST/v1/predict5 cr
POST/v1/sessions10 cr
POST/v1/sessions/{id}/predict4 cr
POST/v1/sessions/{id}/resetFree
DELETE/v1/sessions/{id}Free
Utilities
POST/v1/ground3 cr
POST/v1/parseFree
Management
GET/v1/modelsFree
GET/v1/usageFree
GET/v1/sessionsFree
Surcharges — added to the base fee
Per trajectory screenshot (each prior image sent)+2 cr ($0.02)
Per HD image — strictly larger than 1280×720 (current + trajectory)+1 cr ($0.01)
v1 engine (cua_version: "v1") — v3 / v4 add nothing+3 cr ($0.03)
system_prompt over 500 chars (exactly 500 = no fee)+1 cr ($0.01)
1 credit = $0.01. Base fees: predict 5 cr ($0.05) · session create 10 cr ($0.10, never carries surcharges) · session predict 4 cr ($0.04) · ground 3 cr ($0.03, HD fee only — single image) · parse Free. Charged up-front, auto-refunded if the call fails (PREDICTION_FAILED / GROUNDING_FAILED).

Automate Any Screen

predict, ground and sessions are screen-agnostic: a screenshot goes in, coordinates and actions come out. The pixels can come from anywhere — your own desktop, a browser, a phone emulator, a VNC frame. Coasty VMs are one execution target, not the only one.

Anything that renders pixels is automatable
Your own desktopmss / screencapture / PowerShell — execute with pyautogui
A browser pagePlaywright or Puppeteer screenshot — execute with page.mouse / keyboard
A phone emulatoradb exec-out screencap — execute with adb input tap / text
A remote VNC / RDP frameframebuffer grab — execute by injecting input on the remote
A Coasty cloud VM/v1/machines does the whole loop for you, screenshots included

Coordinates & scaling — read this first

Coordinates come back in the SAME space as the screenshot you sent. If you downscale (e.g. a 2560x1440 desktop resized to 1280x720 to save a credit), multiply returned x/y by your scale factor before clicking — and pass the DOWNSCALED size as screen_width/height. Sending full resolution with the real width/height also works (coordinates map 1:1) and costs +1 credit above 1280x720. Mismatched screenshot vs screen_width/height is the number-one cause of "it clicks the wrong place".

Safety on a real desktop

You are giving a model control of a real mouse and keyboard. Keep pyautogui.FAILSAFE on (mouse to a corner aborts), run with a step cap, send an Idempotency-Key on every predict so a network retry can never double-execute a step, and use the 'Cautious (non-destructive)' preset when the screen can reach anything irreversible.

The Local Agent Loop

Screenshot → predict → execute → repeat until status leaves continue. The full pattern below runs on YOUR machine: sessions keep history server-side, the Idempotency-Key makes every step retry-safe, and the executor maps each returned action to a real input event.

full agent loop on your screen
# Automate YOUR screen — no VM needed. pip install requests mss pyautogui pillow
import base64, io, time, uuid, requests, mss, pyautogui
from PIL import Image

API, KEY = "https://coasty.ai/v1", "sk-coasty-test-..."  # test key = free while you build
HDRS = {"X-API-Key": KEY}
pyautogui.FAILSAFE = True          # slam the mouse into a corner to abort instantly

REAL_W, REAL_H = pyautogui.size()  # your actual desktop resolution
SEND_W, SEND_H = 1280, 720         # what we tell the model (SD = 1 credit cheaper)
SX, SY = REAL_W / SEND_W, REAL_H / SEND_H   # scale model coords -> real pixels

def screenshot_b64():
    with mss.mss() as sct:
        shot = sct.grab(sct.monitors[1])                      # primary monitor
        img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
        img = img.resize((SEND_W, SEND_H))                    # MUST match screen_width/height
        buf = io.BytesIO(); img.save(buf, format="PNG")
        return base64.b64encode(buf.getvalue()).decode()

def execute(a):
    t, p = a["action_type"], a["params"]
    if t == "click":
        pyautogui.click(p["x"] * SX, p["y"] * SY,
                        clicks=p.get("clicks", 1), button=p.get("button", "left"))
    elif t == "type_text":  pyautogui.write(p["text"], interval=0.02)
    elif t == "key_press":  pyautogui.press(p["keys"])
    elif t == "key_combo":  pyautogui.hotkey(*p["keys"])
    elif t == "scroll":     pyautogui.scroll(p["clicks"])     # +up / -down, like pyautogui
    elif t == "drag":
        pyautogui.moveTo(p["x1"] * SX, p["y1"] * SY)
        pyautogui.dragTo(p["x2"] * SX, p["y2"] * SY, duration=0.4)
    elif t == "wait":       time.sleep(p["seconds"])
    return t

# A session keeps screenshot history so the model remembers what it already did.
sess = requests.post(f"{API}/sessions", headers=HDRS, json={
    "cua_version": "v3",
    "screen_width": SEND_W, "screen_height": SEND_H,
    # Optional best-practice steering (Starter+). Pick a preset from the docs:
    "instructions": "Click the visual center of elements. If the target is not visible, scroll toward it, never guess.",
}).json()
sid = sess["session_id"]

task = "Open the calculator and compute 42 * 17"
try:
    for step in range(25):
        r = requests.post(
            f"{API}/sessions/{sid}/predict",
            headers={**HDRS, "Idempotency-Key": f"step-{sid}-{step}-{uuid.uuid4().hex[:8]}"},
            json={"screenshot": screenshot_b64(), "instruction": task},
            timeout=120,
        ).json()
        print(f"step {r['step']}: {r['reasoning'][:80]}")
        for a in r["actions"]:
            if execute(a) in ("done", "fail"):
                raise SystemExit(f"finished: {r['status']} — {r['reasoning']}")
        time.sleep(0.5)                                       # let the UI settle
finally:
    requests.delete(f"{API}/sessions/{sid}", headers=HDRS)    # stop the session clock
Executing every action type on a desktop (pyautogui)
clickx, y, button?=left, clicks?=1pyautogui.click(x, y, clicks=p.get("clicks", 1), button=p.get("button", "left"))
movex, ypyautogui.moveTo(x, y)
type_texttextpyautogui.write(p["text"], interval=0.02)
key_presskeys (list, pressed in order)pyautogui.press(p["keys"])
key_combokeys (held together)pyautogui.hotkey(*p["keys"])
scrollclicks (+up / −down), direction?=vertical, x?, y?pyautogui.scroll(p["clicks"]) — or pyautogui.hscroll() for horizontal
dragx1, y1, x2, y2, button?pyautogui.moveTo(x1, y1); pyautogui.dragTo(x2, y2, duration=0.4)
waitsecondstime.sleep(p["seconds"])
donetask finished — stop the loop
failagent is blocked — stop and inspect `reasoning`
rawcode (pyautogui source)fallback: log it; exec only if you trust the sandbox

Prompt Presets

Best-practice steering for the `instructions` field — appended to the base agent prompt (unlike system_prompt, which replaces it). Pick the preset that matches your job, copy it, and pass it on session create or any predict call. Custom prompts require Starter or higher.

Default pick — careful clicking on real desktops where mis-clicks have consequences.

instructions — Precise UI control
Be precise. Before clicking, confirm the target element is actually visible in the CURRENT screenshot — never click from memory of a previous screen. Click the visual center of elements, not their edges. If the element you need is not visible, scroll toward where it should be instead of guessing coordinates. If two elements look similar, prefer the one whose text matches the task exactly. After typing into a field, verify focus landed in the right field before continuing.
using a preset
requests.post(f"{API}/sessions", headers=HDRS, json={
    "cua_version": "v3",
    "screen_width": 1280, "screen_height": 720,
    "instructions": PRESET,   # the text above — applies to every step in the session
})

instructions is additive steering on top of the tuned base prompt — start here.system_prompt fully replaces the base prompt: more power, more ways to break grounding — reach for it only when a preset plus task phrasing can't express what you need. Both draw from the same per-tier character budget (Starter 2,000 / Pro 4,000 / Enterprise 16,000; +1 credit / $0.01 per call when the prompt is strictly over 500 chars — exactly 500 is free).

Machines API

Provision a sandbox or production VM, then drive it with actions, terminal commands, browser automation, or file operations. Sandbox keys (sk-coasty-test-*) return mock VMs with no AWS billing.

Scopes
machines:readlist, get, screenshot
machines:writeprovision, start, stop, terminate
actions:execclick, type, scroll, browser_*
terminal:execshell command execution
files:readread, exists, list
files:writewrite, edit, append, delete
browser:executearbitrary JS in browser
snapshots:writecreate AMI snapshots
connection:readfetch SSH key + VNC password
Pricing
Provision — wallet gate, no fee20 cr ($0.20) min
VM runtime — Linux, running5 cr/hr ($0.05)
VM runtime — Windows, running9 cr/hr ($0.09)
starting / stopping / restartingrunning rate
VM stopped or suspended (storage only)1 cr/hr ($0.01)
creating / error / terminatedFree
Auto-destroy TTL (ttl_minutes)Free
Snapshot create (one-time)1 cr ($0.01)
Out of funds → VM auto-stoppednever destroyed
Sandbox (sk-coasty-test-*)Free
Metered per minute against your API wallet (1 cr = $0.01), rounded down — partial minutes and partial credits are never billed. All per-call machine endpoints (actions, batch, terminal, browser, files, screenshot, connection, lifecycle) are Free — you pay only the hourly runtime. Live table: GET /v1/machines/pricing.
TipUse a sk-coasty-test-* key during development — you get instant mock VMs (id mch_test_…), synthetic action results, and zero billing. The wire format matches production exactly, so you can swap to a live key and ship.

Provision & Lifecycle

Create a VM, list your fleet, and control start/stop/restart/snapshot/terminate. Set ttl_minutes for auto-destroy (extend or clear any time via PATCH). Runtime bills your API wallet per minute at a small surplus over cloud cost. Sandbox keys mock everything in-memory; live keys provision real EC2 / Azure instances.

provision a vm — python
import requests

# Provision a fresh Linux desktop VM. Sandbox keys (sk-coasty-test-*)
# return a mock machine instantly with no AWS billing.
r = requests.post(
    "https://coasty.ai/v1/machines",
    headers={
        "X-API-Key": "sk-coasty-live-...",
        "Idempotency-Key": "provision-bot-001",   # safe to retry
    },
    json={
        "display_name": "automation-bot",
        "os_type": "linux",
        "desktop_enabled": True,
    },
)
machine = r.json()["machine"]
print(machine["id"], machine["status"])
Lifecycle
GET/v1/machines
GET/v1/machines/{id}
GET/v1/machines/pricing
PATCH/v1/machines/{id}
POST/v1/machines/{id}/start
POST/v1/machines/{id}/stop
POST/v1/machines/{id}/restart
POST/v1/machines/{id}/snapshot
DELETE/v1/machines/{id}

Actions & Batches

Dispatch a single action, or chain up to 50 in one batch. Commands are validated against an explicit allowlist — typos return 422, never reach the VM.

single action — python
import requests

machine_id = "..."  # from provision response
r = requests.post(
    f"https://coasty.ai/v1/machines/{machine_id}/actions",
    headers={"X-API-Key": "sk-coasty-live-..."},
    json={
        "command": "click",
        "parameters": {"x": 512, "y": 340},
    },
)
result = r.json()
print(result["success"], result["duration_ms"], "ms")
Common Commands
clickactions:exec
typeactions:exec
key_pressactions:exec
key_comboactions:exec
scrollactions:exec
dragactions:exec
screenshotactions:exec
terminal_executeterminal:exec
file_readfiles:read
file_writefiles:write
browser_navigateactions:exec
browser_clickactions:exec
browser_executebrowser:execute
batch action — request body
POST /v1/machines/{id}/actions/batch
Content-Type: application/json
X-API-Key: sk-coasty-live-...

{
  "steps": [
    { "command": "browser_navigate",
      "parameters": { "url": "https://example.com/login" } },
    { "command": "browser_type",
      "parameters": { "selector": "#email", "text": "[email protected]" } },
    { "command": "browser_type",
      "parameters": { "selector": "#password", "text": "***" } },
    { "command": "browser_click",
      "parameters": { "selector": "button[type=submit]" } }
  ],
  "stop_on_error": true
}

Returns:
{
  "results": [...],         // one per step
  "completed_count": 4,
  "failed_count": 0,
  "aborted": false,
  "request_id": "req_..."
}

Browser, Terminal, Files

Typed convenience endpoints over /actions. Same dispatch path, ergonomic URL shapes, identical scope rules.

shell command — python
import requests

# Run a shell command (PowerShell on Windows, bash on Linux).
# Output is truncated VM-side to 5000 chars.
r = requests.post(
    f"https://coasty.ai/v1/machines/{machine_id}/terminal",
    headers={"X-API-Key": "sk-coasty-live-..."},
    json={
        "command": "uname -a && uptime",
        "timeout_ms": 10_000,
    },
)
print(r.json()["result"]["output"])
/browser/{op}
opennavigateclicktypedomclickablesstateinfoscrollclosescreenshotwaitlist-tabsopen-tabclose-tabswitch-tab

Body: { parameters: {…}, timeout_ms? }. browser_execute NOT here — use /actions with browser:execute.

/files/{op}
Read (files:read)
readexistslistlist-directorydownloadlist-downloads
Write (files:write)
writeeditappenddeletedelete-directory
/terminal
Body: { command, timeout_ms?, session_id?, cwd? }PowerShell on Windows, bash on Unix. Output capped at 5000 chars VM-side. Pass session_id to reuse a persistent shell across calls.Requires terminal:exec scope.

Machines Endpoints

Full reference. All require X-API-Key (or Authorization: Bearer) except /health.

Lifecycle
POST/v1/machines20 cr gate
GET/v1/machinesFree
GET/v1/machines/{id}Free
GET/v1/machines/pricingFree
PATCH/v1/machines/{id}Free
DELETE/v1/machines/{id}Free
POST/v1/machines/{id}/startFree
POST/v1/machines/{id}/stopFree
POST/v1/machines/{id}/restartFree
POST/v1/machines/{id}/snapshot1 cr
Actions
POST/v1/machines/{id}/actionsFree
POST/v1/machines/{id}/actions/batchFree
POST/v1/machines/{id}/browser/{op}Free
POST/v1/machines/{id}/terminalFree
POST/v1/machines/{id}/files/{op}Free
Inspection
GET/v1/machines/{id}/screenshotFree
GET/v1/machines/{id}/connectionFree
GET/v1/machines/healthFree

Agents API

Hand Coasty a machine and a task; it drives the agent loop for you, streams lifecycle, and pauses for a human when needed. Workflows compose many runs with a versioned JSON DSL. Sandbox keys (sk-coasty-test-*) run against mock VMs with no billing.

Scopes
runs:readlist, get, stream run events
runs:writestart, cancel, resume (human takeover)
workflows:readlist/get workflows + workflow runs
workflows:writecreate, update, delete, start runs
All four are granted to new keys by default. Run/workflow ids are mode-isolated: a test key never sees a live id and vice versa.
Two primitives
Run
One task on one machine. Coasty runs the agent loop, streams events, and emits HMAC-signed lifecycle webhooks.
Workflow
A versioned JSON DSL that composes many runs: task · assert · if · loop · parallel · human_approval · retry · succeed · fail.
BillingBilled responses carry X-Credits-Charged + X-Credits-Remaining headers, and the body usage object exposes credits_charged + cost_cents (both 0 on test keys). Each agent step costs 5 cr ($0.05) on v3/v4 and 8 cr ($0.08) on v1, charged from your API wallet after the step completes (idempotent per step; a failed charge stops the run with WALLET_EXHAUSTED). Starting a run requires wallet balance ≥ one step's cost. Each workflow task step is itself a run with identical per-step billing; control-flow steps (assert · if · loop · parallel · retry · human_approval · succeed · fail) are Free. Total spend is capped by budget_cents (default 0 = no budget guard) and max_iterations (≤ 1000). Test-mode runs bill 0.

Task Runs

POST /v1/runs starts a durable run and returns status 'queued' plus a one-time webhook_secret (only when you pass webhook_url). Idempotency-Key makes retries safe. Resume only works while status == awaiting_human.

start a run — python
import requests

API_KEY = "sk-coasty-live-..."

# Start a task run. Returns status "queued" plus a ONE-TIME webhook_secret
# (only present when you pass webhook_url). Idempotency-Key makes retries safe.
r = requests.post(
    "https://coasty.ai/v1/runs",
    headers={
        "X-API-Key": API_KEY,
        "Idempotency-Key": "invoice-run-4821",
    },
    json={
        "machine_id": "550e8400-e29b-41d4-a716-446655440000",
        "task": "Open the invoice in the browser and read the total.",
        "cua_version": "v3",          # "v4" needs professional tier or above
        "max_steps": 40,
        "on_awaiting_human": "pause", # pause | fail | cancel
        "webhook_url": "https://your.app/hooks/coasty",
    },
)
run = r.json()
print(run["id"], run["status"])               # ... queued
webhook_secret = run.get("webhook_secret")    # shown once — store it now
Request Body
machine_iduuidrequired
taskstringrequired
cua_version"v3" | "v4" | "v1" (8 cr/step)
max_stepsint (1-1000)
on_awaiting_human"pause"|"fail"|"cancel"
webhook_urlstring (https)
Header Idempotency-Key (≤ 128 chars) makes a retried POST return the original run. webhook_secret is returned ONCE on create, so store it; it is null on get/list.
POST /v1/runs — 201 response
{
  "id": "...",
  "status": "queued",
  "machine_id": "550e8400-...",
  "task": "Open the invoice ...",
  "cua_version": "v3",
  "max_steps": 40,
  "on_awaiting_human": "pause",
  "step_count": 0,
  "awaiting_human_reason": null,
  "result": null,
  "error": null,
  "created_at": "2026-06-08T17:00:00Z",
  "usage": { "credits_charged": 0, "cost_cents": 0 },
  "webhook_secret": "whsec_shown_once"
}
Status Lifecycle
queued → running → (awaiting_human ⇄ running) → succeeded | failed | cancelled | timed_out

awaiting_human is only reached when on_awaiting_human == "pause". Terminal states are immutable. POST /v1/runs/{id}/resume is valid only while awaiting a human (otherwise 409 NOT_AWAITING_HUMAN). POST /v1/runs/{id}/cancel works at any non-terminal state.

cancel + resume — python
import requests

# Cancel a run at any non-terminal state.
requests.post(f"https://coasty.ai/v1/runs/{run_id}/cancel",
              headers={"X-API-Key": API_KEY})

# Resume ONLY works when status == "awaiting_human" (human takeover). Resuming
# any other state returns 409 NOT_AWAITING_HUMAN.
requests.post(
    f"https://coasty.ai/v1/runs/{run_id}/resume",
    headers={"X-API-Key": API_KEY},
    json={"note": "Approved by ops; the total is correct."},
)
GET /v1/runs — list (filterable)
GET /v1/runs?status=running&limit=20
X-API-Key: sk-coasty-live-...

# Query params:
#   status = queued|running|awaiting_human|succeeded
#            |failed|cancelled|timed_out   (else 400 INVALID_STATUS_FILTER)
#   limit  = 1..200                        (else 400 INVALID_LIMIT)
#
# GET /v1/runs/{id} fetches one run (404 RUN_NOT_FOUND if not yours).

Run Events & Webhooks

Stream lifecycle live over SSE, or receive HMAC-signed lifecycle webhooks. Both let you react the instant a run needs a human or finishes.

stream events (SSE) — python
import requests

# Stream lifecycle as Server-Sent Events. Reconnect with Last-Event-ID to
# replay everything after the last seq you saw — no gaps on a dropped socket.
with requests.get(
    f"https://coasty.ai/v1/runs/{run_id}/events",
    headers={"X-API-Key": API_KEY, "Last-Event-ID": "42"},
    stream=True,
) as r:
    for line in r.iter_lines(decode_unicode=True):
        if line.startswith("data:"):
            print(line[5:].strip())   # status / step / awaiting_human / terminal
GET /v1/runs/{id}/events

Server-Sent Events. With curl, pass -N to disable buffering so frames arrive live.

Each frame has an id: (monotonic seq). Reconnect with Last-Event-ID: <seq> (or ?after=<seq>) to replay everything after that point, with no gaps on a dropped socket.

Event names: status · step · awaiting_human · billing · terminal.

Lifecycle Webhooks (HMAC)

Pass webhook_url on create (HTTPS only) and Coasty POSTs a signed payload on every lifecycle transition.

Verify with the per-run webhook_secret (returned once):

Coasty-Signature: t=<ts>,v1=<hex>
v1 = HMAC-SHA256(secret, "<t>.<body>")

Events: run.awaiting_human · run.succeeded · run.failed · run.cancelled · run.timed_out.

Workflows & DSL

A versioned JSON DSL composing many runs with branching, loops, parallelism, asserts, retries, and human approvals. {{var}} references pull from earlier steps' results.

create a workflow — python
import requests

# A workflow is a versioned JSON DSL composing many runs with branching,
# loops, parallelism, asserts, retries, and human approvals. {{var}} pulls
# from earlier steps' results (bound via save_as / step id).
r = requests.post(
    "https://coasty.ai/v1/workflows",
    headers={
        "X-API-Key": "sk-coasty-live-...",
        "Idempotency-Key": "ar-collections-v1",
    },
    json={
        "name": "AR collections",
        "definition": {
            "steps": [
                {"id": "invoice", "type": "task", "save_as": "invoice",
                 "machine_id": "550e8400-e29b-41d4-a716-446655440000",
                 "task": "Open the invoice and read its status + total."},
                {"id": "check", "type": "assert",
                 "condition": {"op": "truthy", "value": "{{invoice.passed}}"},
                 "message": "Agent failed to read the invoice"},
                {"id": "branch", "type": "if",
                 "condition": {"op": "contains",
                               "left": "{{invoice.result}}", "right": "PAID"},
                 "then": [{"id": "ok", "type": "succeed",
                           "output": {"state": "paid"}}],
                 "else": [{"id": "ask", "type": "human_approval",
                           "message": "Invoice unpaid — send reminder?"},
                          {"id": "fin", "type": "succeed",
                           "output": {"state": "reminded"}}]},
            ]
        },
    },
)
workflow = r.json()
print(workflow["id"], workflow["version"])
Step Types
taskRun an agent task. Binds result via save_as + step id.
assertFail the workflow unless a condition holds.
ifBranch on a condition: then / else.
loopRepeat a body (count, or while a condition).
parallelRun independent branches concurrently.
human_approvalPause for a human to approve / reject.
retryRetry a body on failure.
succeedFinish successfully with optional output.
failFinish as failed with a message.
Only task steps bill — 5 cr ($0.05) on v3/v4, 8 cr ($0.08) on v1. All other step types are Free.
Condition Ops
eqneltgtltegtecontainstruthyfalsyexistsandornot

Structured + injection-safe (no free-text eval). and/or take conditions: [...]; not takes a single condition.

Validation Limits
Max steps200
Max nesting depth8 levels
Max parallel branches16
human_approval, succeed, and fail are not allowed inside a parallel branch.
run a workflow — python
import requests

# Start a run of a SAVED workflow ...
r = requests.post(
    f"https://coasty.ai/v1/workflows/{workflow_id}/runs",
    headers={"X-API-Key": "sk-coasty-live-...", "Idempotency-Key": "wf-run-9"},
    json={"inputs": {"invoice_url": "https://billing.example.com/inv/42"}},
)
wf_run = r.json()
print(wf_run["id"], wf_run["status"])   # ... queued

# ... or run an AD-HOC inline definition without saving it first:
requests.post(
    "https://coasty.ai/v1/workflows/runs",
    headers={"X-API-Key": "sk-coasty-live-..."},
    json={"definition": {"steps": [
        {"id": "t", "type": "task",
         "machine_id": "550e8400-e29b-41d4-a716-446655440000",
         "task": "Take a screenshot and describe the screen."}]}},
)

Agents Endpoints

Full reference for runs + workflows. All require X-API-Key (or Authorization: Bearer). POST runs/workflows accept an Idempotency-Key.

Task Runs
POST/v1/runs5–8 cr/step
GET/v1/runsFree
GET/v1/runs/{id}Free
GET/v1/runs/{id}/eventsFree
POST/v1/runs/{id}/cancelFree
POST/v1/runs/{id}/resumeFree
Workflows
POST/v1/workflowsFree
GET/v1/workflowsFree
GET/v1/workflows/{id}Free
PUT/v1/workflows/{id}Free
DELETE/v1/workflows/{id}Free
POST/v1/workflows/{id}/runs5–8 cr/step
POST/v1/workflows/runs5–8 cr/step
GET/v1/workflows/runsFree
GET/v1/workflows/runs/{id}Free
GET/v1/workflows/runs/{id}/eventsFree
POST/v1/workflows/runs/{id}/cancelFree
POST/v1/workflows/runs/{id}/resumeFree

Schedules API

Cron-fired agent runs, one-shot run_at jobs, plus three trigger kinds (webhook, email, chain). Schedules created via API show up in your /schedules dashboard automatically.

Scopes
schedules:readlist, get, runs, triggers
schedules:writecreate, update, delete, pause, run-now
triggers:writeadd/remove webhook, email, chain triggers
Pricing
Schedule create — wallet gate, no fee20 cr ($0.20) min
Fire / run-now — wallet gate, no fee20 cr ($0.20) min
Agent runtime per fire (credit balance)10 cr/min
Webhook fire — no routing fee (60/min limit)Free
Email fire — no routing feeFree
Chain trigger (no extra cost)Free
Pause / resume / list / runsFree
Sandbox (sk-coasty-test-*)Free
Gates check your API wallet (1 cr = $0.01) but never charge it. Execution time is billed to your Coasty account credit balance — the same balance the app uses — at 10 credits/min, minimum 20 credits to start, max 6 h per session. Webhook fires charge no routing fee: they require the owner's wallet ≥ 20 cr ($0.20) and are rate-limited (default 60/min per webhook). Test schedules record credits_charged: 0.
Frequency presets
every_15_minutes · every_30_minutes · hourly · every_6_hours · every_12_hours · daily · weekly · monthly · custom
Trigger kinds
webhook (HMAC) · email (inbound mailbox) · chain (fire when another schedule completes; max depth 5)
Run history
Each fire records {status, trigger, duration, credits, error, executed_at}. Cursor-paginated up to 100 retained per schedule.

Create & Lifecycle

Create a cron or one-shot schedule. Pause, resume, run-now, list runs, soft-delete. Idempotency-Key supported on POST.

create a schedule — python
import requests

# Daily 9:00 AM ET email summary, fired by the Coasty scheduler.
# Per fire: needs >= 20 cr ($0.20) in your API wallet to dispatch (gate only);
# agent runtime then bills your Coasty credit balance at 10 credits/min.
r = requests.post(
    "https://coasty.ai/v1/schedules",
    headers={
        "X-API-Key": "sk-coasty-live-...",
        "Idempotency-Key": "morning-briefing-001",
    },
    json={
        "name": "morning briefing",
        "machine_id": "550e8400-e29b-41d4-a716-446655440000",
        "task_prompt": "Summarize unread Gmail and post the top 5 to Slack.",
        "frequency": "daily",
        "time": "09:00",
        "timezone": "America/New_York",
    },
)
schedule = r.json()
print(schedule["id"], schedule["next_run_at"])
Frequency Presets
every_15_minutes*/15 * * * *
every_30_minutes*/30 * * * *
hourly0 * * * *
every_6_hours0 */6 * * *
every_12_hours0 */12 * * *
daily0 9 * * * (override with `time`)
weekly0 9 * * 1 (override `time`, `day_of_week`)
monthly0 9 1 * * (override `time`, `day_of_month`)
customsupply your own `cron` field
one-shot — fire once at a specific UTC time
POST /v1/schedules
Content-Type: application/json
X-API-Key: sk-coasty-live-...

{
  "name": "launch announcement",
  "machine_id": "550e8400-e29b-41d4-a716-446655440000",
  "task_prompt": "Post the launch tweet from the draft.",
  "run_at": "2099-01-01T17:00:00Z"
}

# Notes:
#   * `run_at` and `frequency` are mutually exclusive.
#   * Must be in the future (within last 60s tolerated).
#   * After firing once, the schedule auto-pauses with paused_reason='one_shot_complete'.
LifecycleSchedules are auto-paused after max_consecutive_failures (default 5) failed runs. Resume via POST /v1/schedules/{id}/resume. Insufficient credits at fire-time auto-pauses with reason insufficient_credits.

Triggers

Three trigger kinds. Webhook secrets are returned ONCE — store them on creation.

add a webhook trigger — python
# Add a webhook trigger — returns the signing secret ONCE.
r = requests.post(
    f"https://coasty.ai/v1/schedules/{schedule_id}/triggers",
    headers={"X-API-Key": "sk-coasty-live-..."},
    json={"kind": "webhook", "rate_limit_per_minute": 60},
)
trigger = r.json()
webhook_url    = trigger["webhook_url"]      # https://coasty.ai/v1/triggers/webhook/whk_...
webhook_secret = trigger["webhook_secret"]   # whsec_<64 hex>  — STORE THIS

# Save webhook_secret in your secrets manager. We hash + persist it; we
# cannot show it again. Lose it = generate a new trigger.
{ kind: "webhook" }
Returns webhook_url + webhook_secret (whsec_64hex). Sign every fire with HMAC-SHA256(secret, "{ts}.body") and send Coasty-Signature: t={ts},v1={sig}. Replay window 5 min. Idempotent on identical (id, body) within 60 s.
{ kind: "email" }
Provisions a unique <label>.<rand>@agents.coasty.ai address. Inbound emails fire the schedule. email_label must match ^[a-z0-9][a-z0-9._-]{0,32}[a-z0-9]$.
{ kind: "chain" }
Fire this schedule when source_schedule_id completes. Events: on_complete · on_failure · on_any.Max chain depth: 5.
Trigger Endpoints
GET/v1/schedules/{id}/triggers
POST/v1/schedules/{id}/triggers
DELETE/v1/schedules/{id}/triggers/{trigger_id}
POST/v1/triggers/email-mailbox

Public Webhook Fire

POST /v1/triggers/webhook/{webhook_id} — UNAUTHENTICATED but HMAC-verified. Hit by Stripe, Linear, n8n, anything that can sign a request.

sign + fire a webhook — python
# Customer-side webhook signing — produces a Coasty-Signature header
# the public /v1/triggers/webhook/{id} endpoint accepts.
import hmac, hashlib, time

def sign_coasty_webhook(secret: str, body: bytes) -> dict:
    ts = int(time.time())
    signed_payload = f"{ts}.".encode("utf-8") + body
    sig = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
    return {"Coasty-Signature": f"t={ts},v1={sig}"}

# Fire the webhook from your own app:
import requests
body = b'{"event":"order.placed","order_id":"123"}'
headers = {**sign_coasty_webhook(webhook_secret, body), "Content-Type": "application/json"}
requests.post(webhook_url, data=body, headers=headers)
header format
Coasty-Signature: t=<unix_ts>,v1=<hmac_sha256_hex>

# t        = current unix timestamp (seconds)
# v1       = lowercase hex HMAC-SHA256(webhook_secret, "<t>.<body>")
#            (period as separator; raw body bytes; no newline)
# replay   = signatures > 5 min stale are rejected
# dedup    = identical (webhook_id, body) within 60 s returns deduplicated=true
# body cap = 1 MB (413 if exceeded)
example response
HTTP/1.1 200 OK
Content-Type: application/json
X-Coasty-Request-Id: req_...
X-Coasty-Webhook-Deduplicated: false

{
  "received": true,
  "schedule_id": "550e8400-...",
  "run_id": "550e8400-...",
  "deduplicated": false,
  "message": "Schedule fire dispatched.",
  "request_id": "req_..."
}
SecurityTreat webhook_secret like a password — it grants the ability to fire your schedule. Coasty stores it server-side and uses it to verify every inbound signature. If leaked: delete the trigger and re-create to rotate.

Schedules Endpoints

Full reference. Public webhook fire endpoint is the only one without auth (it uses HMAC-signed Coasty-Signature instead).

Lifecycle
POST/v1/schedules20 cr gate
GET/v1/schedulesFree
GET/v1/schedules/{id}Free
PATCH/v1/schedules/{id}Free
DELETE/v1/schedules/{id}Free
POST/v1/schedules/{id}/pauseFree
POST/v1/schedules/{id}/resumeFree
POST/v1/schedules/{id}/run20 cr gate
History
GET/v1/schedules/{id}/runsFree
GET/v1/schedules/{id}/runs/{run_id}Free
Triggers
GET/v1/schedules/{id}/triggersFree
POST/v1/schedules/{id}/triggersFree
DELETE/v1/schedules/{id}/triggers/{trigger_id}Free
POST/v1/triggers/email-mailboxFree
POST/v1/triggers/webhook/{webhook_id}Free

MCP Server

Drive Coasty from any MCP-capable client — Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot. One install, every Coasty tool available.

What is MCP?

MCP (Model Context Protocol) is the open standard, designed by Anthropic and adopted across the agent ecosystem, that lets LLM hosts plug into external tools and data. Coasty's MCP server is a thin wrapper over the /v1 API — same scopes, same billing. It runs locally via npx; your API key never touches a Coasty MCP relay.

Package
npm i -g @coasty/mcp

Or just point your MCP host at npx -y @coasty/mcp — zero install needed. Set COASTY_API_KEY in the host config and you're running. Sandbox keys (sk-coasty-test-*) work for free.

What you can do from your editor
Predict
Hand the agent a screenshot, get back a sequence of typed actions (click, type, scroll, ...) with exact coordinates.
Drive a VM
Provision a sandbox VM, run terminal commands, navigate a browser, edit files — all from chat.
Schedule a job
Set up a cron job, attach a webhook trigger, hand the secret to your AI to wire into Stripe / Linear / anything.

Install in your MCP host

Pick your client. Configs are checked into the @coasty/mcp test suite — copying any of these blocks verbatim produces a working install.

Claude Desktop — claude_desktop_config.json
// macOS:    ~/Library/Application Support/Claude/claude_desktop_config.json
// Windows:  %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "coasty": {
      "command": "npx",
      "args": ["-y", "@coasty/mcp"],
      "env": { "COASTY_API_KEY": "sk-coasty-test-..." }
    }
  }
}

// Restart Claude Desktop. Coasty tools appear under the 🛠 icon.
Claude Code (CLI)
claude mcp add coasty \
  --env COASTY_API_KEY=sk-coasty-test-... \
  -- npx -y @coasty/mcp

# Verify
claude mcp list
# coasty                     ✓ connected   (24 tools, 2 prompts)
Cursor — .cursor/mcp.json (project) or ~/.cursor/mcp.json (global)
{
  "mcpServers": {
    "coasty": {
      "command": "npx",
      "args": ["-y", "@coasty/mcp"],
      "env": { "COASTY_API_KEY": "sk-coasty-test-..." }
    }
  }
}

// Cursor → Settings → MCP shows a green dot when reachable.
Windsurf — ~/.codeium/windsurf/mcp_config.json
{
  "mcpServers": {
    "coasty": {
      "command": "npx",
      "args": ["-y", "@coasty/mcp"],
      "env": { "COASTY_API_KEY": "sk-coasty-test-..." }
    }
  }
}
VS Code Copilot (Agent mode) — .vscode/mcp.json
// VS Code uses "servers" (NOT "mcpServers"). Tools only appear in
// Agent mode — type # in the chat to autocomplete tool names.
{
  "servers": {
    "coasty": {
      "command": "npx",
      "args": ["-y", "@coasty/mcp"],
      "env": { "COASTY_API_KEY": "sk-coasty-test-..." }
    }
  }
}
TipUse a sk-coasty-test-* key while you're iterating — everything works (provision, schedule, run) but no real EC2 / Azure / credit billing. Swap in sk-coasty-live-* when you're ready to ship.

Tools the MCP server exposes

23 tools across Predict, Machines, Schedules, and Account. All carry MCP annotations (readOnly / destructive / idempotent) so well-behaved hosts confirm before destructive operations.

Predict
coasty_predictScreenshot + goal → list of actions
coasty_groundElement description → (x, y) coords
coasty_parsepyautogui code → structured actions (free)
Machines
coasty_list_machinesRead-only — your VMs
coasty_get_machineRead-only — one VM
coasty_take_machine_screenshotRead-only — current desktop image
coasty_provision_machineCreate new VM (idempotent w/ key)
coasty_terminate_machineDestructive — irreversible
coasty_start_machineResume a stopped VM
coasty_stop_machinePause running VM (preserves state)
coasty_execute_machine_actionDispatch click / type / scroll / browser_* / file_* / etc.
coasty_run_terminal_commandShell exec on VM (terminal:exec scope)
Schedules
coasty_list_schedulesRead-only
coasty_get_scheduleRead-only
coasty_list_schedule_runsCursor-paginated history
coasty_create_scheduleCron, run-once, or custom — appears in dashboard
coasty_update_schedulePATCH (any field)
coasty_delete_scheduleDestructive — soft-delete
coasty_run_schedule_nowManual fire (idempotent w/ key)
coasty_pause_scheduleDisable future fires
coasty_resume_scheduleRe-enable
coasty_add_triggerWebhook / email / chain (HMAC secret returned ONCE)
coasty_remove_triggerDestructive
Account
coasty_get_creditsRead-only — balance + tier + period usage
Prompts
start_automation_session — pre-fill a chat that picks a VM, screenshots, predicts, and executes toward a goal.
debug_failed_run — investigate why a schedule has been failing; proposes concrete fixes.
Annotations
Every tool advertises readOnlyHint, destructiveHint, idempotentHint, and openWorldHint so a well-configured host can auto-approve safe reads and require explicit consent for destructive ops.

Error Handling

Every error returns the same envelope and an X-Coasty-Request-Id header. The code is stable; the message is for humans. Use code + status to branch.

error envelope — every failed request
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This key lacks the scope this route requires.",
    "type": "forbidden",
    "request_id": "req_8f2c1e9a",
    "suggestion": "Re-mint the key with runs:write at /developers.",
    "docs_url": "https://coasty.ai/api-docs#errors",
    "required_scope": "runs:write",        // extra context varies by code
    "current_scopes": ["runs:read"]
  }
}
Always present

Body fields: code, message, type, request_id, suggestion, docs_url, plus code-specific context (e.g. required_scope, balance, details).

Headers: X-Coasty-Request-Id (quote it in support tickets) and Link: <docs_url>; rel="help".

Auth failures also send WWW-Authenticate: Bearer; transient errors (timeouts, upstream outages) send Retry-After.

Auto-refunded codes (PREDICTION_FAILED, GROUNDING_FAILED) refund the charge, so you are not billed for a failed model call.

Troubleshooting: first-week mistakes
401
Auth header wrong
Send your key as X-API-Key OR Authorization: Bearer, but never paste the literal "Bearer " prefix into the X-API-Key value.
402
Out of credits
Add funds, or develop against a sk-coasty-test- key. Test keys never bill and use mock VMs.
403
Missing scope
The key is valid but lacks the route's scope. Re-mint it with the needed scope at /developers; old keys are not upgraded in place.
422
Bad screenshot / missing field
Strip any data: URI prefix before base64; error.details carries the exact failing field path.
400INVALID_LIMITlimit query param out of range; must be 1..200
400INVALID_STATUS_FILTERUnknown ?status= value on a list endpoint
400FEATURE_NOT_AVAILABLEThe feature is gated off for your tier or this mode
401INVALID_API_KEYMissing/invalid key. Send X-API-Key OR Authorization: Bearer (sends WWW-Authenticate). Don't paste "Bearer " into X-API-Key
402INSUFFICIENT_CREDITSWallet below required (returns required + balance). Add funds, or use a sk-coasty-test- key
402WALLET_EXHAUSTEDThe API wallet hit zero mid-request
403INSUFFICIENT_SCOPEKey valid but lacks scope (returns required_scope + current_scopes). Re-mint at /developers
404NOT_FOUNDResource id does not exist or isn't yours
404SESSION_NOT_FOUNDSession id unknown (mode-isolated; test ids never match live)
404RUN_NOT_FOUNDRun id unknown or not owned by your key (mode-isolated)
404WORKFLOW_NOT_FOUNDWorkflow id unknown or not owned by your key (mode-isolated)
409INVALID_STATEIllegal transition (returns current_state + allowed_from)
409NOT_AWAITING_HUMANResumed a run/step that wasn't paused for a human
409RESUME_CONFLICTTwo resumes raced; only the first wins
409IDEMPOTENCY_KEY_REUSEDSame Idempotency-Key reused with a different request body
413PAYLOAD_TOO_LARGEBase64 body over the 10 MB cap
422VALIDATION_ERRORBody failed validation; error.details = field path that failed
422INVALID_SCREENSHOTScreenshot not valid base64; strip the data: prefix first
500INTERNAL_ERRORUnexpected server error; retry, and quote the request_id if it persists
500PREDICTION_FAILEDModel run failed; the charge is auto-refunded
500GROUNDING_FAILEDGrounding failed; auto-refunded
503UPSTREAM_UNAVAILABLEA dependency is down; retry with backoff
504UPSTREAM_TIMEOUTUpstream timed out; retry (use Idempotency-Key on POSTs)

Ship your first click in minutes.

Free account, free keys, free credits to start. No card required.

Coasty - #1 Computer-Use AI Agent | Best for Desktop & Browser Automation