Coasty API

API reference

Build agents that see and act. The full Computer Use API: stateless prediction, stateful sessions, autonomous task runs, and multi-step workflows.

llms.txt
Building with an AI assistant?
Generate a ready-made prompt tailored to Cursor, Claude Code, ChatGPT, or any LLM.

Introduction

The Coasty Computer Use API gives your code the ability to see a screen and act on it. You send a screenshot and a plain-language instruction; the model returns a precise list of actions — clicks, keystrokes, scrolls, and drags — with exact pixel coordinates. Your program performs those actions, captures a new screenshot, and asks again. That loop is how an agent drives any interface, real or virtual, without brittle selectors or per-app scripts.

Everything is a normal HTTPS request to https://coasty.ai/v1. There is no SDK to install and no websocket to manage for the core endpoints: each call is stateless unless you opt into a session. Responses are JSON and stream nothing, so any HTTP client works.

StepWhat happens
1. CaptureTake a screenshot of the screen you want to control and base64-encode it.
2. PredictPOST it with an instruction to /predict. You get back a list of actions.
3. ActExecute those actions on your machine, VM, or browser.
4. RepeatCapture a fresh screenshot and call again until status is done.

Authentication

Every request must include your secret key. The canonical way is the X-API-Key header, but Authorization: Bearer <key> works too: a blank X-API-Key falls through to the Bearer header. Pick one form and send the raw key. Do not paste the literal text Bearer  inside X-API-Key; that is the single most common first-day mistake and it returns 401 INVALID_API_KEY. Keys are created and revoked from the API keys page. Treat a key like a password: keep it server-side, store it in an environment variable, and never commit it or ship it in client-side code.

Header
X-API-Key: sk-coasty-live-your_key_here
PrefixKindBehaviour
sk-coasty-live-LiveRuns the real model and draws down your USD wallet balance.
sk-coasty-test-TestReturns mock responses and never bills. Ideal for local dev and CI.
Prefer test keys while you wire up your integration. An sk-coasty-test- key never bills and runs against mock VMs, yet exercises the exact same request and response shapes (its X-Credits-Charged and usage.cost_cents are always 0), so you can build and run CI confidently before flipping to a live key.

Quickstart

Your first prediction is four steps: export a key, capture a screenshot, base64-encode it, and POST it with an instruction. Grab a test key from the API keys page (it never bills) and set it in your shell:

Shell
export COASTY_API_KEY="sk-coasty-test-your_key_here"

Now send the prediction. The call below uploads a screenshot, asks the model to click a button, and prints the actions it returns. Pick your language:

import base64, os, requests

API_KEY = os.environ["COASTY_API_KEY"]

with open("screen.png", "rb") as f:
    screenshot = base64.b64encode(f.read()).decode()

res = requests.post(
    "https://coasty.ai/v1/predict",
    headers={"X-API-Key": API_KEY},
    json={
        "screenshot": screenshot,
        "instruction": "Click the login button",
        "screen_width": 1920,
        "screen_height": 1080,
    },
    timeout=60,
)
res.raise_for_status()
data = res.json()

print(data["status"])              # "continue" | "done" | "fail"
for action in data["actions"]:
    print(action["action_type"], action["params"])

A successful response contains an actions array and a status of continue, done, or fail. Execute each action in order, take a new screenshot, and call again while the status is continue. That loop is the whole API in miniature.

Want to...Go to
Run a multi-step task without resending historySessions
Hand the agent a task and let it drive to done on its ownTask runs
Chain many tasks with branches, loops, and guardsWorkflows
Map a failed call to a fixErrors
Each of those sections ships a complete, copy-pasteable flow in all six languages: a sessions loop, a run polled to completion, and a workflow created then run. Start from the one that matches your task and adapt it.

Predict

POST /v1/predict is the stateless workhorse. Each call is independent: you provide the full context every time, which makes it simple to reason about and trivial to scale horizontally. Use it for one-shot decisions and for loops where you manage history yourself. When a task needs the model to remember prior steps automatically, reach for sessions instead.

FieldTypeRequiredDescription
screenshotstringYesBase64-encoded PNG or JPEG of the current screen.
instructionstringYesNatural-language goal, e.g. "Click the login button".
screen_widthintNoWidth in pixels (default 1920). Improves coordinate accuracy.
screen_heightintNoHeight in pixels (default 1080).
max_actionsintNoCap on actions returned per call (default 5).
toolsstring[]NoRestrict to a subset of action types, e.g. ["click", "type_text"].
include_reasoningboolNoReturn the model's reasoning string (default true).

The response is the standard prediction shape, covered in Response format.

Sessions

A session keeps the trajectory — the running history of screenshots and actions — on our side, so each step only needs the latest screenshot and instruction. This produces better multi-step behaviour on long tasks and keeps your request bodies small. Create a session once, step through the task, then delete it to release your concurrency quota.

import base64, os, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}

def screenshot() -> str:
    with open("screen.png", "rb") as f:
        return base64.b64encode(f.read()).decode()

# 1. Open a session — it remembers the trajectory across steps
session = requests.post(f"{BASE}/sessions", headers=HEADERS, json={
    "screen_width": 1920,
    "screen_height": 1080,
}, timeout=60).json()
session_id = session["session_id"]

# 2. Drive the task one step at a time
try:
    for _ in range(20):  # safety cap
        res = requests.post(
            f"{BASE}/sessions/{session_id}/predict",
            headers=HEADERS,
            json={
                "screenshot": screenshot(),
                "instruction": "Book a meeting tomorrow at 3pm",
            },
            timeout=60,
        ).json()

        for action in res["actions"]:
            perform(action)          # your action executor

        if res["status"] != "continue":
            break
finally:
    # 3. Always release the session to free your concurrency quota
    requests.delete(f"{BASE}/sessions/{session_id}", headers=HEADERS, timeout=30)
EndpointPurpose
POST /v1/sessionsCreate a session. Returns a session_id valid for 24h of inactivity.
POST /v1/sessions/{id}/predictPredict the next step. Body is just screenshot + instruction.
POST /v1/sessions/{id}/resetClear history to start a new task on the same session. Free.
DELETE /v1/sessions/{id}End the session and free a concurrency slot. Free.
Always delete a session in a finally block. Sessions count against your tier's concurrent-session limit, and orphaned sessions only expire after 24 hours of inactivity.

Grounding

Grounding answers a narrower question than predict: “where is this element?” Give it a screenshot and a description and it returns the exact x, y coordinate to target. It is faster and cheaper than a full prediction, which makes it ideal when you already know what to do and only need a pixel to click.

import os, requests

res = requests.post(
    "https://coasty.ai/v1/ground",
    headers={"X-API-Key": os.environ["COASTY_API_KEY"]},
    json={
        "screenshot": screenshot,   # base64 PNG (see Quickstart)
        "element": "the blue Submit button below the form",
    },
    timeout=60,
).json()

print(res["x"], res["y"])           # exact click coordinates

The response is { x, y, usage, request_id }. Coordinates are in the same pixel space as the screenshot you sent.

OCR

OCR extracts every piece of visible text from a screenshot, each with its bounding box. Use it to assert that a page reached the expected state, to scrape values, or to feed text into your own logic. Returns a flat full_text string plus an elements array of { text, left, top, width, height }.

import os, requests

res = requests.post(
    "https://coasty.ai/v1/ocr",
    headers={"X-API-Key": os.environ["COASTY_API_KEY"]},
    json={"screenshot": screenshot},   # base64 PNG (see Quickstart)
    timeout=60,
).json()

print(res["full_text"])
for el in res["elements"]:
    print(repr(el["text"]), "at", (el["left"], el["top"]))

Parse

Parse converts a block of pyautogui code into the same structured action objects the model returns. It is deterministic, runs no model, and is free. Use it to migrate existing automation scripts onto Coasty's executor, or to normalise hand-written steps into the canonical action schema.

import os, requests

res = requests.post(
    "https://coasty.ai/v1/parse",
    headers={"X-API-Key": os.environ["COASTY_API_KEY"]},
    json={"code": "pyautogui.click(100, 200)\npyautogui.typewrite('hello')"},
    timeout=30,
).json()

for action in res["actions"]:
    print(action["action_type"], action["params"])

Task runs

A run hands the agent a task and a machine, then drives it to completion on our side. The agent loops autonomously, verifies its own work (pass or fail), can pause for a human when it hits a wall, bills per step from your dollar API wallet, and streams every event live. You start one call and watch, instead of running the predict loop yourself.

Create a run with POST /v1/runs. The two required fields are machine_id and task. The response is an agent.run object with status of queued, plus a one-time webhook_secret you store to verify webhooks. Send an Idempotency-Key header to make a retried create safe.

import os, time, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}
TERMINAL = {"succeeded", "failed", "cancelled", "timed_out"}

# 1. Start a run. Idempotency-Key makes a retried create safe.
run = requests.post(
    f"{BASE}/runs",
    headers={**HEADERS, "Idempotency-Key": "order-4821"},
    json={
        "machine_id": "m_9f2c",
        "task": "Open the billing page and download the latest invoice as PDF",
        "cua_version": "v3",         # "v4" needs professional tier or above
        "max_steps": 40,
        "on_awaiting_human": "pause",
    },
    timeout=30,
).json()
run_id = run["id"]
print(run["status"])                 # "queued"
webhook_secret = run.get("webhook_secret")   # shown once; store it now

# 2. Poll until terminal.
while True:
    run = requests.get(f"{BASE}/runs/{run_id}", headers=HEADERS, timeout=30).json()
    print(run["status"], run["steps_completed"], "steps")
    if run["status"] in TERMINAL:
        break
    time.sleep(2)

print(run["result"])                 # {"passed": ..., "status": ..., "summary": ...}
FieldRequiredDescription
machine_idYesThe machine the agent will drive.
taskYesThe natural-language goal to accomplish.
cua_versionNoModel family. v3 by default; v4 needs professional tier or above.
instructionsNoExtra guidance appended to the base prompt.
system_promptNoA preamble placed ahead of the base prompt.
max_stepsNoHard cap on agent steps (default 50).
deadline_secondsNoWall-clock budget; the run becomes timed_out if breached.
on_awaiting_humanNoWhat to do when a human is needed: pause (default), fail, or cancel.
awaiting_human_timeout_secondsNoHow long to wait for a human before timing out.
webhook_urlNoHTTPS endpoint for lifecycle callbacks (https only).
metadataNoArbitrary JSON echoed back on the run object.
EndpointPurpose
POST /v1/runsStart a run. Returns the run plus a one-time webhook_secret.
GET /v1/runsList runs. Filter with ?status= and ?limit=.
GET /v1/runs/{id}Fetch a single run and its current status.
GET /v1/runs/{id}/eventsServer-Sent Events stream of the run (see Streaming events).
POST /v1/runs/{id}/cancelCancel a run that has not reached a terminal state.
POST /v1/runs/{id}/resumeHand control back after a human takeover.
JSON
{
  "id": "run_7a1b2c3d",
  "object": "agent.run",
  "status": "queued",
  "machine_id": "m_9f2c",
  "task": "Open the billing page and download the latest invoice as PDF",
  "cua_version": "v3",
  "instructions": null,
  "max_steps": 40,
  "on_awaiting_human": "pause",
  "steps_completed": 0,
  "credits_charged": 0,
  "cost_cents": 0,
  "result": null,
  "error": null,
  "awaiting_human_reason": null,
  "metadata": {
    "team": "finance"
  },
  "webhook_url": "https://example.com/hooks/coasty",
  "created_at": "2026-06-01T12:00:00Z",
  "started_at": null,
  "awaiting_human_since": null,
  "finished_at": null,
  "request_id": "req_4f9a2b1c",
  "webhook_secret": "whsec_one_time_value_shown_here"
}
FieldTypeDescription
idstringUnique run id, prefixed run_.
objectstringAlways "agent.run".
statusstringqueued, running, awaiting_human, succeeded, failed, cancelled, or timed_out.
machine_idstringThe machine the agent is driving.
taskstringThe natural-language goal you submitted.
cua_versionstringModel family: "v3" (default) or "v4" (professional tier and above).
instructionsstringExtra guidance appended to the base prompt (nullable).
max_stepsintHard cap on agent steps (default 50).
on_awaiting_humanstringWhat to do when a human is needed: pause, fail, or cancel.
steps_completedintHow many agent steps have run so far.
credits_chargedintInternal cost units billed (1 unit = $0.01). See cost_cents for the dollar amount.
cost_centsintDollar cost so far, in cents (USD).
resultobject{ passed, status, summary, verdict? } once the run finishes.
errorobject{ code, message } when the run failed (nullable).
awaiting_human_reasonstringWhy the run paused for a human (nullable).
metadataobjectThe metadata you attached at create time.
webhook_urlstringWhere lifecycle events are POSTed (nullable).
created_atstringISO-8601 creation timestamp.
started_atstringWhen the run left the queue (nullable).
awaiting_human_sincestringWhen the run last paused for a human (nullable).
finished_atstringWhen the run reached a terminal state (nullable).
request_idstringId of the create request, for support and tracing.
A run moves through queued to running, can bounce between running and awaiting_human, and ends in one of succeeded, failed, cancelled, or timed_out. Terminal states are immutable, so it is always safe to stop polling once you reach one. Runs need the runs:read and runs:write scopes, granted to new keys by default.

Streaming events

GET /v1/runs/{id}/events returns a Server-Sent Events stream so you can follow a run as it happens, instead of polling. Each event has a type and a numeric id (the sequence number). If your connection drops, reconnect and replay everything you missed by sending the last sequence you saw as a Last-Event-ID header, or as the ?after= query parameter. The stream closes after the done event.

import os, httpx

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}
run_id = "run_7a1b"
last_seq = 0  # persist this so a reconnect can replay

# httpx streams the SSE body line by line. Reconnect with Last-Event-ID.
with httpx.stream(
    "GET",
    f"{BASE}/runs/{run_id}/events",
    headers={**HEADERS, "Last-Event-ID": str(last_seq)},
    timeout=None,
) as resp:
    event_type = "message"
    for line in resp.iter_lines():
        if line.startswith("id:"):
            last_seq = int(line[3:].strip())
        elif line.startswith("event:"):
            event_type = line[6:].strip()
        elif line.startswith("data:"):
            data = line[5:].strip()
            print(event_type, data)
            if event_type == "done":
                break
EventMeaning
statusThe run moved to a new status (running, awaiting_human, succeeded, etc.).
textA chunk of the agent's natural-language narration.
reasoningA chunk of the model's private reasoning, if exposed.
tool_callThe agent invoked a tool (a click, a keypress, a navigation).
tool_resultThe result of the most recent tool call.
awaiting_humanThe run paused and is waiting for a human to take over.
resumedControl was handed back after a human takeover.
stepA full agent step completed; carries steps_completed.
billingIncremental billing update (credits_charged, cost_cents).
errorA non-fatal or fatal error occurred during the run.
doneTerminal event. The stream closes after this is sent.

Human takeover

Some steps need a person: a captcha, a one-time code, a judgment call. When the agent reaches one and on_awaiting_human is pause, the run moves to awaiting_human and emits an awaiting_human event with a reason. A human completes the blocking step (in the same machine session), then you hand control back with POST /v1/runs/{id}/resume and an optional note. Resume is only valid while the status is awaiting_human.

import os, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}
run_id = "run_7a1b"

run = requests.get(f"{BASE}/runs/{run_id}", headers=HEADERS, timeout=30).json()

# resume is only valid while status == "awaiting_human".
if run["status"] == "awaiting_human":
    print("paused:", run["awaiting_human_reason"])
    # ... a human completes the blocking step out of band ...
    resumed = requests.post(
        f"{BASE}/runs/{run_id}/resume",
        headers=HEADERS,
        json={"note": "Solved the captcha; continue"},
        timeout=30,
    ).json()
    print(resumed["status"])         # back to "running"
Detect the pause from either the run object (status == awaiting_human with awaiting_human_reason set), the SSE awaiting_human event, or the run.awaiting_human webhook. After resume, the run returns to running and emits a resumed event. Set on_awaiting_human to fail or cancel at create time if you would rather the run stop than wait for a human.

Webhooks

Pass a webhook_url (https only) when you create a run and we POST a signed callback at each lifecycle transition. The response to your create call includes a webhook_secret exactly once: store it, because every callback is signed with it. Each request carries a Coasty-Signature header of the form t=<unix_ts>,v1=<hex>.

To verify, build the signed payload as "<t>." + raw_request_body, compute HMAC-SHA256 over it keyed by the webhook_secret, and compare against v1 with a constant-time check. Always hash the raw body bytes, before any JSON re-serialisation.

import hashlib, hmac, os, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}

# 1. Create a run with a webhook_url. webhook_secret is returned exactly once.
run = requests.post(
    f"{BASE}/runs",
    headers=HEADERS,
    json={
        "machine_id": "m_9f2c",
        "task": "Reconcile the invoice against the order",
        "webhook_url": "https://example.com/hooks/coasty",
    },
    timeout=30,
).json()
webhook_secret = run["webhook_secret"]   # persist this securely

# 2. In your webhook handler, verify the Coasty-Signature header.
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

# Example (your framework supplies the raw body + header):
# ok = verify(request.body, request.headers["Coasty-Signature"], webhook_secret)
EventMeaning
run.awaiting_humanThe run paused and needs a human to take over.
run.succeededThe run finished and verification passed.
run.failedThe run ended in failure (verification failed or an error).
run.cancelledThe run was cancelled via the cancel endpoint.
run.timed_outThe run breached its deadline before finishing.

Workflows

A workflow composes many runs into one versioned program, with branching, loops, and guards expressed as a JSON DSL. Each task step is itself an agent run, so a workflow is the way to chain tasks, gate them on conditions, and pass results between them. Workflows are versioned: re-creating the same slug bumps the version, and a PUT does too.

Create one with POST /v1/workflows. The slug must match [a-z0-9_-]. The response is a Workflow carrying an id, a version, and the current dsl_version (2026-06-01).

import os, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}

definition = {
    "steps": [
        {
            "id": "fetch",
            "type": "task",
            "task": "Open order {{inputs.order_id}} and read the invoice total",
            "save_as": "invoice",
        },
        {
            "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": "no", "type": "fail", "message": "Invoice not marked paid"}],
        },
    ],
}

# 1. Create the workflow. Re-using the same slug bumps its version.
wf = requests.post(
    f"{BASE}/workflows",
    headers=HEADERS,
    json={
        "name": "Invoice reconciliation",
        "slug": "invoice-reconcile",
        "inputs_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}},
        "definition": definition,
    },
    timeout=30,
).json()
print(wf["id"], "v", wf["version"], wf["dsl_version"])

# 2. Start a run of the saved workflow.
run = requests.post(
    f"{BASE}/workflows/{wf['id']}/runs",
    headers=HEADERS,
    json={"inputs": {"order_id": "ord_4821"}, "machine_id": "m_9f2c", "budget_cents": 500},
    timeout=30,
).json()
print(run["id"], run["status"])
EndpointPurpose
POST /v1/workflowsCreate a workflow (or bump its version when the slug already exists).
GET /v1/workflowsList workflows. Filter with ?limit=.
GET /v1/workflows/{id}Fetch a workflow and its definition.
PUT /v1/workflows/{id}Replace the definition; bumps the version.
DELETE /v1/workflows/{id}Archive a workflow.
Workflows need the workflows:read and workflows:write scopes, granted to new keys by default. See the Workflow DSL for the full step and condition catalogue.

Workflow DSL

The DSL (dsl_version 2026-06-01) is a JSON object with a steps array and an optional output. Each step has an id and a type. A task step runs the agent and binds its result ({ status, passed, result, run_id, steps, error }) under both its save_as name and its step id, so later steps can read it.

JSON
{
  "dsl_version": "2026-06-01",
  "definition": {
    "steps": [
      {
        "id": "fetch",
        "type": "task",
        "task": "Open order {{inputs.order_id}} and read the invoice total",
        "save_as": "invoice"
      },
      {
        "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": "no",
            "type": "fail",
            "message": "Invoice not marked paid"
          }
        ]
      }
    ],
    "output": {
      "paid": "{{invoice.result}}"
    }
  }
}
Step typeShapeDescription
task{ task, machine_id?, save_as? }Run an agent task. Supports {{var}} templating. Binds its result under save_as and the step id.
assert{ condition, message? }Fail the workflow unless the structured condition holds.
if{ condition, then, else? }Branch on a structured condition.
loop{ count | while, body }Repeat a body a fixed number of times or while a condition holds.
parallel{ branches: [[...], [...]] }Run independent branches concurrently.
human_approval{ message?, timeout_seconds? }Pause for a human to approve or reject before continuing.
retry{ body, max_attempts }Retry a body up to max_attempts times on failure.
succeed{ output? }Finish the workflow successfully with an optional output.
fail{ message? }Finish the workflow as failed with an optional message.

Conditions are structured rather than expression strings, which keeps them injection-safe. Each left, right, or value is either a literal or a {{path}} reference. Paths are dotted lookups into inputs.*, vars.*, and any step id or save_as name.

OperatorShapeDescription
eq / ne{ op, left, right }Equal / not equal.
lt / gt / lte / gte{ op, left, right }Ordered numeric comparison.
contains{ op, left, right }left contains right (substring or membership).
truthy / falsy / exists{ op, value }Test a single value for truthiness, falsiness, or presence.
and / or{ op, conditions: [..] }Combine several conditions.
not{ op, condition }Negate a condition.
Three hard guards stop a workflow run when breached: budget_cents (spend cap in USD cents; 0 means unlimited), max_iterations (loop cap), and deadline_seconds (wall-clock). A breach ends the run as failed or timed_out.

A definition is validated before it is accepted. The limits below are enforced at create and ad-hoc time, so an invalid definition is rejected with 422 VALIDATION_ERROR rather than failing mid-run.

LimitRule
Max stepsA definition holds at most 100 steps in total (counting every nested step).
Max nesting depthSteps can nest at most 8 levels deep (if, loop, parallel, retry bodies).
Parallel branchesA parallel step takes at most 16 branches; they run concurrently.
Retry attemptsretry max_attempts is an integer from 1 to 20.
Parallel contentshuman_approval, succeed, and fail are not allowed inside a parallel branch.
save_as namesave_as must not be "inputs" or "vars" (those namespaces are reserved).
Workflows are version-pinned. When a run starts, the workflow's current definition is snapshotted into that run, so editing or replacing the workflow (which bumps its version) never changes runs already in flight. Each run records the workflow_version it executed.

Running workflows

Start a saved workflow with POST /v1/workflows/{id}/runs, or run a definition inline (without saving) with POST /v1/workflows/runs by adding a definition (and optional inputs_schema) to the same body. Both return a workflow.run. The body accepts inputs, a default machine_id for task steps, and the budget_cents, max_iterations, and deadline_seconds guards. An Idempotency-Key header is honoured here too.

import os, requests

BASE = "https://coasty.ai/v1"
HEADERS = {"X-API-Key": os.environ["COASTY_API_KEY"]}

# POST /v1/workflows/runs runs a definition inline, without saving a workflow.
run = requests.post(
    f"{BASE}/workflows/runs",
    headers=HEADERS,
    json={
        "machine_id": "m_9f2c",
        "inputs": {"url": "https://status.example.com"},
        "max_iterations": 5,
        "definition": {
            "steps": [
                {
                    "id": "open",
                    "type": "task",
                    "save_as": "page",
                    "task": "Open {{inputs.url}} and report whether all systems are operational",
                },
                {
                    "id": "gate",
                    "type": "assert",
                    "condition": {"op": "truthy", "value": "{{page.passed}}"},
                },
            ],
        },
    },
    timeout=30,
).json()
print(run["id"], run["status"])      # object == "workflow.run"
EndpointPurpose
POST /v1/workflows/{id}/runsStart a run of a saved workflow.
POST /v1/workflows/runsRun an inline definition without saving a workflow.
GET /v1/workflows/runsList workflow runs. Filter with ?workflow_id= and ?limit=.
GET /v1/workflows/runs/{id}Fetch a single workflow run.
GET /v1/workflows/runs/{id}/eventsSSE stream with the same Last-Event-ID replay semantics.
POST /v1/workflows/runs/{id}/cancelCancel a workflow run.
POST /v1/workflows/runs/{id}/resumeApprove or reject a human_approval pause with { approved, note? }.
JSON
{
  "id": "wfr_5e6f7a8b",
  "object": "workflow.run",
  "status": "running",
  "workflow_id": "wf_1a2b3c",
  "workflow_version": 3,
  "machine_id": "m_9f2c",
  "inputs": {
    "order_id": "ord_4821"
  },
  "output": null,
  "error": null,
  "awaiting_human_reason": null,
  "awaiting_step_id": null,
  "iterations_used": 0,
  "spent_cents": 0,
  "budget_cents": 500,
  "created_at": "2026-06-01T12:00:00Z",
  "started_at": "2026-06-01T12:00:01Z",
  "finished_at": null,
  "request_id": "req_9c8b7a6d"
}
FieldTypeDescription
idstringUnique workflow-run id, prefixed wfr_.
objectstringAlways "workflow.run".
statusstringqueued, running, awaiting_human, succeeded, failed, cancelled, or timed_out.
workflow_idstringThe workflow this run belongs to (null for inline runs).
workflow_versionintThe version of the workflow definition that ran.
machine_idstringDefault machine for task steps that omit machine_id.
inputsobjectThe inputs you passed in, available as {{inputs.*}}.
outputobjectThe output produced by a succeed step (nullable).
errorobject{ code, message } when the run failed (nullable).
awaiting_human_reasonstringWhy the run paused (nullable).
awaiting_step_idstringThe step id awaiting human approval (nullable).
iterations_usedintLoop iterations consumed against max_iterations.
spent_centsintTotal spend so far, in USD cents.
budget_centsintSpend cap, in USD cents (0 means unlimited).
created_atstringISO-8601 creation timestamp.
started_atstringWhen execution began (nullable).
finished_atstringWhen the run reached a terminal state (nullable).
request_idstringId of the create request, for support and tracing.

Action types

Every action the model can return uses an action_type from the table below, paired with a params object. Your executor switches on the type and applies the parameters. The terminal types — done and fail — set the response status and signal you to stop looping.

ActionParamsDescription
click{ x, y }Single left click at the given pixel coordinate.
type_text{ text }Type a literal string at the current focus.
key_press{ key }Press one key, e.g. "enter", "tab", "escape".
key_combo{ keys: [..] }Press a chord, e.g. ["ctrl", "c"] or ["cmd", "v"].
scroll{ x, y, direction, amount }Scroll up / down / left / right at a position.
drag{ from_x, from_y, to_x, to_y }Press, move, and release between two points.
move{ x, y }Move the cursor without clicking.
wait{ ms }Pause before the next step (e.g. for a page load).
done{}The task is complete. status becomes "done".
fail{ reason? }The task is impossible. status becomes "fail".

Response format

Predict and session-predict return the same shape. actions is the ordered list to execute; status tells you whether to keep going (continue), stop successfully (done), or stop because the task is impossible (fail). usage reports tokens and the dollar cost of the call (cost_cents).

Billed success responses also carry two headers you can read without parsing the body: X-Credits-Charged (what this call cost) and X-Credits-Remaining (your wallet balance after it). In the body, the same numbers appear as usage.credits_charged and usage.cost_cents. On an sk-coasty-test- key both are always 0. Every response (success or error) additionally carries an X-Coasty-Request-Id header that mirrors request_id; quote it when contacting support.

JSON
{
  "request_id": "req_8f2c1e9a",
  "status": "continue",
  "reasoning": "The login form is visible. I'll click the email field, then type the address.",
  "actions": [
    {
      "action_type": "click",
      "params": {
        "x": 512,
        "y": 340
      },
      "description": "Click the email field"
    },
    {
      "action_type": "type_text",
      "params": {
        "text": "[email protected]"
      },
      "description": "Type the email address"
    }
  ],
  "raw_code": [
    "pyautogui.click(512, 340)",
    "pyautogui.typewrite('[email protected]')"
  ],
  "usage": {
    "input_tokens": 1523,
    "output_tokens": 245,
    "credits_charged": 5,
    "cost_cents": 45
  }
}
FieldDescription
request_idUnique id for the call. Include it when contacting support.
statusOne of continue, done, fail.
actionsOrdered list of actions to perform this step.
reasoningThe model's explanation (omitted if include_reasoning is false).
raw_codeThe equivalent pyautogui lines, if you prefer to run those.
usageTokens plus the cost of the request (see the two fields below).
usage.credits_chargedInternal cost units billed (1 unit = $0.01). See cost_cents for the dollar amount.
usage.cost_centsDollar cost so far, in cents (USD).

Errors

Errors return a non-2xx status and a JSON envelope under an error key. The code is stable and safe to branch on; message is human-readable and may change. Every error also carries an error.request_id (mirrored in the X-Coasty-Request-Id response header), plus error.suggestion and error.docs_url for self-service. A Link: <url>; rel="help" header mirrors docs_url. Always log the request id: it is the fastest way for us to trace a failed call.

Some codes attach machine-readable context to the body. A 402 (INSUFFICIENT_CREDITS) reports required and balance; a 403 reports required_scope and current_scopes; a 422 VALIDATION_ERROR lists the offending field path under error.details; and a 409 state conflict carries current_state with allowed_from or required_state.

JSON
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Your API wallet does not have enough funds to complete this request.",
    "type": "payment_required",
    "suggestion": "Add funds in the dashboard, or use an sk-coasty-test- key while building (test keys never bill).",
    "docs_url": "https://coasty.ai/developers/docs#errors",
    "required": 45,
    "balance": 12,
    "request_id": "req_8f2c1e9a"
  }
}
StatusCodeCause and fix
401INVALID_API_KEYKey missing, malformed, or revoked (or "Bearer " was wrongly pasted into X-API-Key). 401s carry a WWW-Authenticate header.
403INSUFFICIENT_SCOPEThe key is valid but lacks the scope this endpoint needs. The body lists required_scope and current_scopes; re-mint a key with the scope.
402INSUFFICIENT_CREDITSYour USD wallet can't cover the request. The body reports required and balance. Add funds, or use a test key while building.
402WALLET_EXHAUSTEDThe wallet emptied mid-run. Steps that already completed were billed; top up to continue.
422VALIDATION_ERRORThe body failed schema validation. error.details lists the offending field path and the expected type.
422INVALID_SCREENSHOTThe screenshot is not decodable base64 PNG or JPEG. Strip any data: prefix and remove whitespace before encoding.
413PAYLOAD_TOO_LARGEThe screenshot exceeds the 10 MB base64 limit. Downscale the image or re-encode it as JPEG.
400INVALID_LIMITA ?limit= query parameter fell outside the allowed range of 1 to 200.
400INVALID_STATUS_FILTERA ?status= query parameter is not one of the real statuses for that resource.
404NOT_FOUNDThe resource id is unknown or expired. Ids are mode-isolated, so a test key can't see live resources.
404SESSION_NOT_FOUNDThe session id is unknown or its 24h inactivity window expired.
404RUN_NOT_FOUNDThe run id is unknown, expired, or belongs to the other key mode.
404WORKFLOW_NOT_FOUNDThe workflow id (or workflow-run id) is unknown or was archived.
409NOT_AWAITING_HUMANYou resumed a run that is not in awaiting_human. The body reports current_state and required_state.
409RESUME_CONFLICTA resume or cancel race was lost (the run already moved on). Re-read the run and retry against its new state.
409IDEMPOTENCY_KEY_REUSEDThe same Idempotency-Key was sent with a different body. Use a fresh key, or replay the original request verbatim.
429RATE_LIMIT_EXCEEDEDA per-key or per-user rate cap was hit. Honor Retry-After. A per_user cap can't be raised by minting more keys.
429TOO_MANY_RUNSThe concurrent-run cap for your tier was reached. Wait for a run to finish, then retry.
400FEATURE_NOT_AVAILABLEThe feature is gated to a higher tier (for example cua_version v4). Upgrade the plan or drop the gated option.
500INTERNAL_ERRORAn unexpected server error. Retry, and quote request_id when contacting support.
500PREDICTION_FAILEDThe prediction model run failed. The charge is automatically refunded.
500GROUNDING_FAILEDThe grounding model run failed. The charge is automatically refunded.
500OCR_FAILEDThe OCR model run failed. The charge is automatically refunded.
503UPSTREAM_UNAVAILABLEA transient upstream outage. Retry with an Idempotency-Key and exponential backoff.
504UPSTREAM_TIMEOUTAn upstream call timed out. Transient; retry with an Idempotency-Key.
Treat 429, 503 (UPSTREAM_UNAVAILABLE), and 504 (UPSTREAM_TIMEOUT) as retryable: honor Retry-After on a 429, and use an Idempotency-Key with exponential backoff on the upstream codes. A 500 model failure (PREDICTION_FAILED, GROUNDING_FAILED, OCR_FAILED) auto-refunds the charge, so retrying is free.

Troubleshooting

Five mistakes account for almost every first-week support ticket. Each maps to one status and one fix:

SymptomLikely causeFix
401Wrong header. The key is missing, or Bearer  was pasted into X-API-Key.Send the raw key in X-API-Key, or use Authorization: Bearer <key>. Never both prefixes.
402No credits. Your live wallet can't cover the call (INSUFFICIENT_CREDITS).Add funds, or build against an sk-coasty-test- key (test keys never bill).
403Missing scope. The key lacks required_scope for this endpoint.Re-mint a key with the needed scope (for example runs:write or workflows:write).
422Bad screenshot or missing field. Undecodable base64, a data: prefix, or an absent required field.Strip the data: prefix and whitespace; read error.details for the exact field path.
429Rate limited. A per-key or per-user cap (RATE_LIMIT_EXCEEDED) was hit.Back off and honor Retry-After. A per_user cap can't be raised by minting more keys.

Rate limits

Limits apply per key and, in aggregate, per user. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (a Unix timestamp) so you can pace requests precisely rather than guessing. When you exceed a limit you get 429 RATE_LIMIT_EXCEEDED with a Retry-After header: honor it before retrying. The per_user cap is shared across all your keys, so minting more keys does not raise it. A separate 429 TOO_MANY_RUNS guards the concurrent-run cap for agent runs.

TierRequests / minConcurrent sessionsTrajectory
Free313 steps
Starter1035 steps
Professional20108 steps
Enterprise3010020 steps

Pricing

Requests are billed in US dollars from your API wallet. The charge is taken before the model runs and automatically refunded if a request fails server-side. Internally each request unit is $0.01 (the granularity behind every price below), but everything you pay and see is dollars. High-resolution screenshots (above 1280×720) and longer trajectories add a small surcharge; test keys are always free.

EndpointCostNotes
POST /v1/predict$0.05Stateless prediction.
POST /v1/sessions$0.10One-time session creation.
POST /v1/sessions/{id}/predict$0.04Each step inside a session.
POST /v1/ground$0.03Coordinate grounding.
POST /v1/ocr$0.03Text extraction.
POST /v1/parseFreeDeterministic, no model call.
POST /v1/runs$0.05/stepPer agent step on v3/v4 (v1 is $0.08), billed from your dollar API wallet.
POST /v1/workflows/runs$0.05/stepEach task step is a run; total capped by budget_cents.
API reference — Coasty Computer Use API | Coasty - AI Computer-Use Agent