{"slug": "launch-hn-coasty-yc-s26-an-api-for-computer-use-agents", "title": "Launch HN: Coasty (YC S26) – An API for computer-use agents", "summary": "YC-backed startup Coasty launched an API that lets developers run autonomous computer-use agents on managed machines, compose them into workflows, and drop to lower-level prediction primitives when needed. The API supports task runs, workflows, stateful sessions, and stateless prediction steps, with test keys available for development.", "body_md": "# API reference\n\nRun autonomous tasks on managed machines, compose them into workflows, and drop to prediction primitives only when you need direct control.\n\n[llms.txt](/docs/llms.txt)\n\n## Introduction\n\nStart with a [task run](#runs): give Coasty a goal and a machine, then let the agent drive to completion. Use [workflows](#workflows) when an automation needs many tasks, branches, loops, approvals, or shared outputs. [Machines](#machines) provide the managed computer those tasks operate.\n\nThe prediction endpoints are lower-level primitives for teams that need to own the control loop themselves. Use [sessions](#sessions) for a stateful screenshot loop, [predict](#predict) for a stateless step, grounding for coordinates, and parse for structured actions. Everything is normal HTTPS to `https://coasty.ai/v1`\n\n, so you can choose the highest-level surface that fits the job and drop down only when you need finer control.\n\n## Authentication\n\nEvery API-key-authenticated request must include your secret key. The four health probes are public, and webhook ingress uses its documented `Coasty-Signature`\n\nHMAC credential instead. For API-key operations, the canonical form is the `X-API-Key`\n\nheader, but `Authorization: Bearer <key>`\n\nworks too: a blank `X-API-Key`\n\nfalls through to the Bearer header. Pick one form and send the raw key. Do not paste the literal text `Bearer `\n\ninside `X-API-Key`\n\n; that is the single most common first-day mistake and it returns `401 INVALID_API_KEY`\n\n. Keys are created and revoked from the [API keys](/developers/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.\n\n```\nX-API-Key: sk-coasty-live-your_key_here\n```\n\n`sk-coasty-test-`\n\nkey never bills and runs against mock VMs, yet exercises the exact same request and response shapes (its `X-Credits-Charged`\n\nand `usage.cost_cents`\n\nare always `0`\n\n), so you can build and run CI confidently before flipping to a live key.## Run your first task\n\nYour first autonomous task needs an API key and a machine. Grab a test key from the [API keys](/developers/keys) page (it never bills), then use an existing machine or [provision one](#machines). Set the key in your shell:\n\n```\nexport COASTY_API_KEY=\"sk-coasty-test-your_key_here\"\n```\n\nStart the task with `POST /v1/runs`\n\n. Replace the example `machine_id`\n\n, describe the outcome in `task`\n\n, and send an `Idempotency-Key`\n\nso a retried create cannot start a duplicate run. The complete example starts the run and follows it to a terminal state:\n\n``` python\nimport os, time, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\nTERMINAL = {\"succeeded\", \"failed\", \"cancelled\", \"timed_out\"}\n\n# 1. Start a run. Idempotency-Key makes a retried create safe.\nrun = requests.post(\n    f\"{BASE}/runs\",\n    headers={**HEADERS, \"Idempotency-Key\": \"order-4821\"},\n    json={\n        \"machine_id\": \"mch_test_0123456789abcdef\",\n        \"task\": \"Open the billing page and download the latest invoice as PDF\",\n        \"cua_version\": \"v5\",         # any of v1/v3/v4/v5, all tiers; omit to use the v5 default\n        \"max_steps\": 40,\n        \"on_awaiting_human\": \"pause\",\n    },\n    timeout=30,\n).json()\nrun_id = run[\"id\"]\nprint(run[\"status\"])                 # \"queued\"\nwebhook_secret = run.get(\"webhook_secret\")   # shown once; store it now\n\n# 2. Poll until terminal.\nwhile True:\n    run = requests.get(f\"{BASE}/runs/{run_id}\", headers=HEADERS, timeout=30).json()\n    print(run[\"status\"], run[\"steps_completed\"], \"steps\")\n    if run[\"status\"] in TERMINAL:\n        break\n    time.sleep(2)\n\nprint(run[\"result\"])                 # {\"passed\": ..., \"status\": ..., \"summary\": ...}\n```\n\nThe create response begins at `queued`\n\n. Coasty then drives the machine, records each step, and finishes as `succeeded`\n\n, `failed`\n\n, `cancelled`\n\n, or `timed_out`\n\n. Your application can poll, subscribe to the event stream, or receive signed webhooks; it does not need to execute each prediction itself.\n\n## Task runs\n\nA 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 $0.05 per completed step from your dollar API wallet ($0.08/step on the legacy v1 engine), and streams every event live. You start one call and watch, instead of running the predict loop yourself.\n\nCreate a run with `POST /v1/runs`\n\n. The two required fields are `machine_id`\n\nand `task`\n\n. The response is an `agent.run`\n\nobject with `status`\n\nof `queued`\n\n, plus a one-time `webhook_secret`\n\nyou store to verify [webhooks](#run-webhooks). Send an `Idempotency-Key`\n\nheader to make a retried create safe.\n\n``` python\nimport os, time, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\nTERMINAL = {\"succeeded\", \"failed\", \"cancelled\", \"timed_out\"}\n\n# 1. Start a run. Idempotency-Key makes a retried create safe.\nrun = requests.post(\n    f\"{BASE}/runs\",\n    headers={**HEADERS, \"Idempotency-Key\": \"order-4821\"},\n    json={\n        \"machine_id\": \"mch_test_0123456789abcdef\",\n        \"task\": \"Open the billing page and download the latest invoice as PDF\",\n        \"cua_version\": \"v5\",         # any of v1/v3/v4/v5, all tiers; omit to use the v5 default\n        \"max_steps\": 40,\n        \"on_awaiting_human\": \"pause\",\n    },\n    timeout=30,\n).json()\nrun_id = run[\"id\"]\nprint(run[\"status\"])                 # \"queued\"\nwebhook_secret = run.get(\"webhook_secret\")   # shown once; store it now\n\n# 2. Poll until terminal.\nwhile True:\n    run = requests.get(f\"{BASE}/runs/{run_id}\", headers=HEADERS, timeout=30).json()\n    print(run[\"status\"], run[\"steps_completed\"], \"steps\")\n    if run[\"status\"] in TERMINAL:\n        break\n    time.sleep(2)\n\nprint(run[\"result\"])                 # {\"passed\": ..., \"status\": ..., \"summary\": ...}\n{\n  \"id\": \"run_7a1b2c3d\",\n  \"object\": \"agent.run\",\n  \"status\": \"queued\",\n  \"machine_id\": \"mch_test_0123456789abcdef\",\n  \"task\": \"Open the billing page and download the latest invoice as PDF\",\n  \"cua_version\": \"v5\",\n  \"instructions\": null,\n  \"max_steps\": 40,\n  \"on_awaiting_human\": \"pause\",\n  \"steps_completed\": 0,\n  \"credits_charged\": 0,\n  \"cost_cents\": 0,\n  \"result\": null,\n  \"error\": null,\n  \"awaiting_human_reason\": null,\n  \"metadata\": {\n    \"team\": \"finance\"\n  },\n  \"webhook_url\": \"https://example.com/hooks/coasty\",\n  \"created_at\": \"2026-06-01T12:00:00Z\",\n  \"started_at\": null,\n  \"awaiting_human_since\": null,\n  \"finished_at\": null,\n  \"request_id\": \"req_4f9a2b1c\",\n  \"webhook_secret\": \"whsec_one_time_value_shown_here\"\n}\n```\n\n`queued`\n\nto `running`\n\n, can bounce between `running`\n\nand `awaiting_human`\n\n, and ends in one of `succeeded`\n\n, `failed`\n\n, `cancelled`\n\n, or `timed_out`\n\n. Terminal states are immutable, so it is always safe to stop polling once you reach one. Runs need the `runs:read`\n\nand `runs:write`\n\nscopes, granted to new keys by default.## Streaming events\n\n`GET /v1/runs/{id}/events`\n\nreturns 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`\n\n(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`\n\nheader, or as the `?after=`\n\nquery parameter. The stream closes after the `done`\n\nevent.\n\n``` python\nimport os, httpx\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\nrun_id = \"run_7a1b\"\nlast_seq = 0  # persist this so a reconnect can replay\n\n# httpx streams the SSE body line by line. Reconnect with Last-Event-ID.\nwith httpx.stream(\n    \"GET\",\n    f\"{BASE}/runs/{run_id}/events\",\n    headers={**HEADERS, \"Last-Event-ID\": str(last_seq)},\n    timeout=None,\n) as resp:\n    event_type = \"message\"\n    for line in resp.iter_lines():\n        if line.startswith(\"id:\"):\n            last_seq = int(line[3:].strip())\n        elif line.startswith(\"event:\"):\n            event_type = line[6:].strip()\n        elif line.startswith(\"data:\"):\n            data = line[5:].strip()\n            print(event_type, data)\n            if event_type == \"done\":\n                break\n```\n\n## Human takeover\n\nSome steps need a person: a captcha, a one-time code, a judgment call. When the agent reaches one and `on_awaiting_human`\n\nis `pause`\n\n, the run moves to `awaiting_human`\n\nand emits an `awaiting_human`\n\nevent 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`\n\nand an optional `note`\n\n. Resume is only valid while the status is `awaiting_human`\n\n.\n\n``` python\nimport os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\nrun_id = \"run_7a1b\"\n\nrun = requests.get(f\"{BASE}/runs/{run_id}\", headers=HEADERS, timeout=30).json()\n\n# resume is only valid while status == \"awaiting_human\".\nif run[\"status\"] == \"awaiting_human\":\n    print(\"paused:\", run[\"awaiting_human_reason\"])\n    # ... a human completes the blocking step out of band ...\n    resumed = requests.post(\n        f\"{BASE}/runs/{run_id}/resume\",\n        headers=HEADERS,\n        json={\"note\": \"Solved the captcha; continue\"},\n        timeout=30,\n    ).json()\n    print(resumed[\"status\"])         # back to \"running\"\n```\n\n`status == awaiting_human`\n\nwith `awaiting_human_reason`\n\nset), the SSE `awaiting_human`\n\nevent, or the `run.awaiting_human`\n\nwebhook. After resume, the run returns to `running`\n\nand emits a `resumed`\n\nevent. Set `on_awaiting_human`\n\nto `fail`\n\nor `cancel`\n\nat create time if you would rather the run stop than wait for a human.## Webhooks\n\nPass a `webhook_url`\n\n(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`\n\nexactly once: store it, because every callback is signed with it. Each request carries a `Coasty-Signature`\n\nheader of the form `t=<unix_ts>,v1=<hex>`\n\n.\n\nTo verify, build the signed payload as `\"<t>.\" + raw_request_body`\n\n, compute `HMAC-SHA256`\n\nover it keyed by the `webhook_secret`\n\n, and compare against `v1`\n\nwith a constant-time check. Always hash the raw body bytes, before any JSON re-serialisation.\n\n``` python\nimport hashlib, hmac, os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\n# 1. Create a run with a webhook_url. webhook_secret is returned exactly once.\nrun = requests.post(\n    f\"{BASE}/runs\",\n    headers=HEADERS,\n    json={\n        \"machine_id\": \"mch_test_0123456789abcdef\",\n        \"task\": \"Reconcile the invoice against the order\",\n        \"webhook_url\": \"https://example.com/hooks/coasty\",\n    },\n    timeout=30,\n).json()\nwebhook_secret = run[\"webhook_secret\"]   # persist this securely\n\n# 2. In your webhook handler, verify the Coasty-Signature header.\ndef verify(raw_body: bytes, signature_header: str, secret: str) -> bool:\n    parts = dict(p.split(\"=\", 1) for p in signature_header.split(\",\"))\n    signed = f\"{parts['t']}.\".encode() + raw_body\n    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts[\"v1\"])\n\n# Example (your framework supplies the raw body + header):\n# ok = verify(request.body, request.headers[\"Coasty-Signature\"], webhook_secret)\n```\n\n## Bring your own model\n\nBy default every LLM call in the computer-use harness runs on Coasty's managed models. BYOK (bring your own key) flips that: opt in and the entire harness (the worker, grounding, the code agent, and compaction; every LLM call) runs on your own Anthropic or OpenAI account instead. Opt-in is always explicit, per request or per stored key. `provider: \"managed\"`\n\n(or omitting `llm`\n\nentirely) keeps the platform default, unchanged.\n\nThere are two ways to hand over a key. Store it once with `PUT /v1/llm/keys/{provider}`\n\n(encrypted with AES-256-GCM at rest; only a sha256-prefix fingerprint is ever echoed back), or send it per request in headers. A header key takes precedence over the stored key. Sending a key without a provider returns `422`\n\n.\n\n```\nX-LLM-Provider: anthropic\nX-LLM-Api-Key: sk-ant-your_key_here\nX-LLM-Model: claude-sonnet-4-6\n```\n\nThe headers work on `POST /v1/predict`\n\n, `POST /v1/runs`\n\n, `POST /v1/workflows/{id}/runs`\n\n, `POST /v1/sessions`\n\n, and `POST /v1/schedules`\n\n. Stored keys are managed through three endpoints, gated by the `llm_keys`\n\nscope (granted to new live keys by default). These endpoints require a live key; sandbox keys cannot read, overwrite, or delete the production credential store:\n\n``` python\nimport os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\n# Store (upsert) your own Anthropic key. Encrypted at rest; never echoed back.\nstored = requests.put(\n    f\"{BASE}/llm/keys/anthropic\",\n    headers=HEADERS,\n    json={\"api_key\": os.environ[\"ANTHROPIC_API_KEY\"]},\n    timeout=30,\n).json()\nprint(stored)   # {\"provider\": \"anthropic\", \"key_fingerprint\": \"a1b2c3d4\", \"stored\": true}\n\n# List stored keys: provider, fingerprint, timestamps. Never the key itself.\nkeys = requests.get(f\"{BASE}/llm/keys\", headers=HEADERS, timeout=30).json()\nfor k in keys[\"keys\"]:\n    print(k[\"provider\"], k[\"key_fingerprint\"])\n\n# Delete when you rotate away (404 LLM_KEY_NOT_FOUND when none is stored)\nrequests.delete(f\"{BASE}/llm/keys/anthropic\", headers=HEADERS, timeout=30)\n```\n\nOn the request body, the same endpoints accept an `llm`\n\nobject that selects the provider and, optionally, a model per harness role. It deliberately has no api_key field (a `422`\n\nif you attempt one): keys ride headers or the encrypted store only, so they can never be echoed in run objects, webhooks, or idempotency replays.\n\n``` python\nimport os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\n# Start a run on YOUR Anthropic key (stored earlier via PUT /llm/keys/anthropic).\n# The llm block deliberately has NO api_key field (422 if you try): keys ride\n# headers or the encrypted store only, never request bodies.\nrun = requests.post(\n    f\"{BASE}/runs\",\n    headers=HEADERS,\n    json={\n        \"machine_id\": \"mch_test_0123456789abcdef\",\n        \"task\": \"Open the billing page and download the latest invoice as PDF\",\n        \"llm\": {\n            \"provider\": \"anthropic\",             # or \"openai\"; \"managed\" = platform default\n            \"model\": \"claude-sonnet-4-6\",        # any model on your account (vision-capable)\n            \"compaction_model\": \"claude-haiku-4-5\",  # optional per-role override\n        },\n    },\n    timeout=30,\n).json()\n\n# Per-request variant: send the key in headers instead of storing it.\n# A header key takes precedence over the stored key for this request only.\nbyok_headers = {\n    **HEADERS,\n    \"X-LLM-Provider\": \"anthropic\",\n    \"X-LLM-Api-Key\": os.environ[\"ANTHROPIC_API_KEY\"],\n    \"X-LLM-Model\": \"claude-sonnet-4-6\",\n}\n\n# The run echoes a non-secret llm block; the key itself is never returned.\nrun = requests.get(f\"{BASE}/runs/{run['id']}\", headers=HEADERS, timeout=30).json()\nprint(run[\"llm\"])   # {\"provider\": ..., \"model\": ..., \"key_fingerprint\": ..., \"key_source\": ..., \"key_scrubbed\": ...}\n```\n\nPer-role overrides exist for tuning cost against quality: run compaction on a cheaper model while the worker stays on the default, for example. For runs, workflows, and schedules the key is snapshotted encrypted into the run, so crash-recovery on another replica keeps using your key; it is scrubbed the moment the run reaches a terminal state. `GET /v1/runs/{id}`\n\nechoes a non-secret `llm`\n\nblock: {`provider, model, key_fingerprint, key_source, key_scrubbed`\n\n}. Schedules store only the non-secret preference; at fire time the current stored key is used. Delete the stored key and future firings fail loudly with `LLM_KEY_NOT_CONFIGURED`\n\n; they never silently run on platform keys.\n\n`grounding_model`\n\nto experiment before committing a cheaper or different model to that role.`LLM_KEY_NOT_CONFIGURED`\n\n, `LLM_KEY_INVALID`\n\n, `LLM_PROVIDER_AUTH_FAILED`\n\n, `LLM_PROVIDER_RATE_LIMITED`\n\n, `LLM_PROVIDER_QUOTA_EXCEEDED`\n\n, and `LLM_PROVIDER_ERROR`\n\n.Billing: Coasty's per-call and per-step platform charges are unchanged with BYOK. The model tokens are billed by your provider account directly.\n\n## Workflows\n\nA workflow composes many runs into one versioned program, with branching, loops, and guards expressed as a JSON DSL. Each `task`\n\nstep 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`\n\nbumps the version, and a `PUT`\n\ndoes too.\n\nCreate one with `POST /v1/workflows`\n\n. The `slug`\n\nmust match `[a-z0-9_-]`\n\n. The response is a `Workflow`\n\ncarrying an `id`\n\n, a `version`\n\n, and the current `dsl_version`\n\n(`2026-06-01`\n\n).\n\n``` python\nimport os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\ndefinition = {\n    \"steps\": [\n        {\n            \"id\": \"fetch\",\n            \"type\": \"task\",\n            \"task\": \"Open order {{inputs.order_id}} and read the invoice total\",\n            \"save_as\": \"invoice\",\n        },\n        {\n            \"id\": \"check\",\n            \"type\": \"assert\",\n            \"condition\": {\"op\": \"truthy\", \"value\": \"{{invoice.passed}}\"},\n            \"message\": \"Agent failed to read the invoice\",\n        },\n        {\n            \"id\": \"branch\",\n            \"type\": \"if\",\n            \"condition\": {\"op\": \"contains\", \"left\": \"{{invoice.result}}\", \"right\": \"PAID\"},\n            \"then\": [{\"id\": \"ok\", \"type\": \"succeed\", \"output\": {\"state\": \"paid\"}}],\n            \"else\": [{\"id\": \"no\", \"type\": \"fail\", \"message\": \"Invoice not marked paid\"}],\n        },\n    ],\n}\n\n# 1. Create the workflow. Re-using the same slug bumps its version.\nwf = requests.post(\n    f\"{BASE}/workflows\",\n    headers=HEADERS,\n    json={\n        \"name\": \"Invoice reconciliation\",\n        \"slug\": \"invoice-reconcile\",\n        \"inputs_schema\": {\"type\": \"object\", \"properties\": {\"order_id\": {\"type\": \"string\"}}},\n        \"definition\": definition,\n    },\n    timeout=30,\n).json()\nprint(wf[\"id\"], \"v\", wf[\"version\"], wf[\"dsl_version\"])\n\n# 2. Start a run of the saved workflow.\nrun = requests.post(\n    f\"{BASE}/workflows/{wf['id']}/runs\",\n    headers=HEADERS,\n    json={\"inputs\": {\"order_id\": \"ord_4821\"}, \"machine_id\": \"mch_test_0123456789abcdef\", \"budget_cents\": 500},\n    timeout=30,\n).json()\nprint(run[\"id\"], run[\"status\"])\n```\n\n`workflows:read`\n\nand `workflows:write`\n\nscopes, granted to new keys by default. See the [Workflow DSL](#workflow-dsl)for the full step and condition catalogue.\n\n## Workflow DSL\n\nThe DSL (`dsl_version`\n\n`2026-06-01`\n\n) is a JSON object with a `steps`\n\narray and an optional `output`\n\n. Each step has an `id`\n\nand a `type`\n\n. A `task`\n\nstep runs the agent and binds its result (`{ status, passed, result, run_id, steps, error }`\n\n) under both its `save_as`\n\nname and its step id, so later steps can read it.\n\n```\n{\n  \"dsl_version\": \"2026-06-01\",\n  \"definition\": {\n    \"steps\": [\n      {\n        \"id\": \"fetch\",\n        \"type\": \"task\",\n        \"task\": \"Open order {{inputs.order_id}} and read the invoice total\",\n        \"save_as\": \"invoice\"\n      },\n      {\n        \"id\": \"check\",\n        \"type\": \"assert\",\n        \"condition\": {\n          \"op\": \"truthy\",\n          \"value\": \"{{invoice.passed}}\"\n        },\n        \"message\": \"Agent failed to read the invoice\"\n      },\n      {\n        \"id\": \"branch\",\n        \"type\": \"if\",\n        \"condition\": {\n          \"op\": \"contains\",\n          \"left\": \"{{invoice.result}}\",\n          \"right\": \"PAID\"\n        },\n        \"then\": [\n          {\n            \"id\": \"ok\",\n            \"type\": \"succeed\",\n            \"output\": {\n              \"state\": \"paid\"\n            }\n          }\n        ],\n        \"else\": [\n          {\n            \"id\": \"no\",\n            \"type\": \"fail\",\n            \"message\": \"Invoice not marked paid\"\n          }\n        ]\n      }\n    ],\n    \"output\": {\n      \"paid\": \"{{invoice.result}}\"\n    }\n  }\n}\n```\n\nConditions are structured rather than expression strings, which keeps them injection-safe. Each `left`\n\n, `right`\n\n, or `value`\n\nis either a literal or a `{{path}}`\n\nreference. Paths are dotted lookups into `inputs.*`\n\n, `vars.*`\n\n, and any step id or `save_as`\n\nname.\n\n`budget_cents`\n\n(spend cap in USD cents; 0 means unlimited), `max_iterations`\n\n(loop cap), and `deadline_seconds`\n\n(wall-clock). A breach ends the run as `failed`\n\nor `timed_out`\n\n.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`\n\nrather than failing mid-run.\n\n`definition`\n\nis snapshotted into that run, so editing or replacing the workflow (which bumps its `version`\n\n) never changes runs already in flight. Each run records the `workflow_version`\n\nit executed.## Running workflows\n\nStart a saved workflow with `POST /v1/workflows/{id}/runs`\n\n, or run a definition inline (without saving) with `POST /v1/workflows/runs`\n\nby adding a `definition`\n\n(and optional `inputs_schema`\n\n) to the same body. Both return a `workflow.run`\n\n. The body accepts `inputs`\n\n, a default `machine_id`\n\nfor task steps, and the `budget_cents`\n\n, `max_iterations`\n\n, and `deadline_seconds`\n\nguards. An `Idempotency-Key`\n\nheader is honoured here too.\n\n``` python\nimport os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\n# POST /v1/workflows/runs runs a definition inline, without saving a workflow.\nrun = requests.post(\n    f\"{BASE}/workflows/runs\",\n    headers=HEADERS,\n    json={\n        \"machine_id\": \"mch_test_0123456789abcdef\",\n        \"inputs\": {\"url\": \"https://status.example.com\"},\n        \"max_iterations\": 5,\n        \"definition\": {\n            \"steps\": [\n                {\n                    \"id\": \"open\",\n                    \"type\": \"task\",\n                    \"save_as\": \"page\",\n                    \"task\": \"Open {{inputs.url}} and report whether all systems are operational\",\n                },\n                {\n                    \"id\": \"gate\",\n                    \"type\": \"assert\",\n                    \"condition\": {\"op\": \"truthy\", \"value\": \"{{page.passed}}\"},\n                },\n            ],\n        },\n    },\n    timeout=30,\n).json()\nprint(run[\"id\"], run[\"status\"])      # object == \"workflow.run\"\n{\n  \"id\": \"wfr_5e6f7a8b\",\n  \"object\": \"workflow.run\",\n  \"status\": \"running\",\n  \"workflow_id\": \"wf_1a2b3c\",\n  \"workflow_version\": 3,\n  \"machine_id\": \"mch_test_0123456789abcdef\",\n  \"inputs\": {\n    \"order_id\": \"ord_4821\"\n  },\n  \"output\": null,\n  \"error\": null,\n  \"awaiting_human_reason\": null,\n  \"awaiting_step_id\": null,\n  \"iterations_used\": 0,\n  \"spent_cents\": 0,\n  \"budget_cents\": 500,\n  \"created_at\": \"2026-06-01T12:00:00Z\",\n  \"started_at\": \"2026-06-01T12:00:01Z\",\n  \"finished_at\": null,\n  \"request_id\": \"req_9c8b7a6d\"\n}\n```\n\n## Provisioning\n\nA machine is a Coasty-managed cloud VM that an agent can see and control. You provision one, poll until it is `running`\n\n, then either hand it to a [task run](#runs) (the agent drives it to done) or drive it yourself with the [action endpoints](#machine-connect). Machines are optional: `/predict`\n\n, `/sessions`\n\n, and `/ground`\n\nrun against *your* screen. You only need a Coasty machine when you want the agent to execute on a VM Coasty hosts.\n\nProvision with `POST /v1/machines`\n\n. Only `display_name`\n\nis required; everything else has a sensible default. The body rejects unknown fields, so a typo returns `422 VALIDATION_ERROR`\n\nrather than being silently ignored.\n\n```\ncurl -X POST https://coasty.ai/v1/machines \\\n  -H \"X-API-Key: $COASTY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"display_name\": \"agent-box\", \"os_type\": \"linux\", \"desktop_enabled\": true, \"ttl_minutes\": 120}'\n```\n\nProvisioning is asynchronous. The response returns immediately with the machine in `creating`\n\nstatus and a `connection`\n\nobject whose secrets are redacted. The VM is not drivable yet — poll `GET /v1/machines/{id}`\n\nuntil `status`\n\nis `running`\n\nbefore you send actions or start a run.\n\n```\n{\n  \"machine\": {\n    \"id\": \"9f2c1e7a-3b6d-4c81-9a0e-2d5f8b1c4e90\",\n    \"display_name\": \"agent-box\",\n    \"status\": \"creating\",\n    \"os_type\": \"linux\",\n    \"desktop_enabled\": true,\n    \"cpu_cores\": 2,\n    \"memory_gb\": 4,\n    \"storage_gb\": 16,\n    \"public_ip\": null,\n    \"auto_destroy_at\": \"2026-06-17T14:30:00Z\",\n    \"ttl_minutes\": 120,\n    \"is_test\": false,\n    \"created_at\": \"2026-06-17T12:30:00Z\"\n  },\n  \"connection\": {\n    \"public_ip\": null,\n    \"ssh_port\": 22,\n    \"ssh_username\": \"ubuntu\",\n    \"has_ssh_key\": true,\n    \"has_vnc_password\": true\n  },\n  \"request_id\": \"req_2f9c1a7b3e4d\"\n}\n```\n\nThe machine object — returned by provision, list, and get:\n\nList your machines with `GET /v1/machines`\n\n(newest first, `?limit=`\n\n1–200, default 50) — it returns `{ data, has_more, request_id }`\n\n. Fetch one with `GET /v1/machines/{id}`\n\n. Both read straight from the registry, so they keep working even when provisioning is busy.\n\n`machines:write`\n\nscope, a wallet balance of at least `20`\n\ncredits (Unlimited-tier keys skip this), and room under your plan's concurrent-machine cap. Building? An `sk-coasty-test-`\n\nkey returns a fully-shaped mock VM (id `mch_test_…`\n\n, `is_test: true`\n\n) with zero billing — up to 5 at a time.## Lifecycle & TTL\n\nA machine moves through a small set of statuses. `status`\n\nis the field you poll: drive the machine only while it is `running`\n\n. The runtime rate that applies in each status is shown below; exact per-hour USD numbers are in the [Pricing](#pricing) section.\n\nStart, stop, restart are `POST /v1/machines/{id}/start`\n\n(and `/stop`\n\n, `/restart`\n\n). They are asynchronous: the call returns a transitional status (`starting`\n\n/ `stopping`\n\n) and you poll until it settles. They are state-checked — starting a machine that is already running, or stopping one that is not running, returns `409 INVALID_STATE`\n\nwith `current_state`\n\nand `allowed_from`\n\nin the body, so you can react without guessing. `start`\n\nis allowed from `stopped`\n\nor `error`\n\n; `stop`\n\nonly from `running`\n\n.\n\nTerminate with `DELETE /v1/machines/{id}`\n\n. This is permanent — the VM and its disk are destroyed and a later GET returns `404 MACHINE_NOT_FOUND`\n\n. Delete is idempotent: deleting an already-gone machine still succeeds, so retries are safe.\n\n```\ncurl -X POST https://coasty.ai/v1/machines/$MACHINE_ID/stop   -H \"X-API-Key: $COASTY_API_KEY\"\ncurl -X DELETE https://coasty.ai/v1/machines/$MACHINE_ID       -H \"X-API-Key: $COASTY_API_KEY\"\n```\n\nAuto-destroy (TTL). A machine left running bills until you destroy it, so set `ttl_minutes`\n\nas a safety net. A background sweep (every ~60s) terminates a machine once its `auto_destroy_at`\n\npasses. Adjust it any time with `PATCH /v1/machines/{id}`\n\n: `ttl_minutes`\n\nis measured *from now* (so it doubles as a lease extension), accepts `5`\n\n–`10080`\n\n(5 min to 7 days), and `0`\n\nclears auto-destroy entirely. Anything else is `400 INVALID_TTL`\n\n. Provisioning without a TTL works but adds a `Warning`\n\nresponse header nudging you to set one.\n\n```\ncurl -X PATCH https://coasty.ai/v1/machines/$MACHINE_ID \\\n  -H \"X-API-Key: $COASTY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"ttl_minutes\": 30}'\n```\n\nRuntime billing. Machines bill the developer API wallet (separate from any subscription credits) by the minute, rounded down to whole credits in your favour. Running Linux is `$0.05/hr`\n\n, running Windows `$0.09/hr`\n\n, and a `stopped`\n\nor `suspended`\n\nmachine the keep-alive rate of `$0.01/hr`\n\n; `creating`\n\n, `error`\n\n, and `terminated`\n\nare free. Transitional states (starting/stopping/restarting) bill at the running rate. The per-call control endpoints (actions, terminal, files, browser, screenshot, connection) are never billed — you pay for runtime only.\n\n`suspended`\n\n) rather than destroying it — your disk and snapshots are preserved. Top up the wallet and `POST /start`\n\nto resume. You can watch live accrual for every metered machine at `GET /v1/billing/active`\n\n.## Connect & control\n\nOnce a machine is `running`\n\nyou can reach it three ways: connect directly over SSH/VNC, drive it through Coasty's control endpoints, or hand it to an agent run. Normal machine responses redact secrets and only expose `has_ssh_key`\n\n/ `has_vnc_password`\n\nbooleans plus ports.\n\nConnection secrets. `GET /v1/machines/{id}/connection`\n\nreturns the full `ssh_private_key_pem`\n\n, `vnc_password`\n\n, public IP, and ports. It is gated by the opt-in `connection:read`\n\nscope (not granted by default — request it when you mint the key), and the response is sent `Cache-Control: no-store`\n\n. Treat that payload like a password: never log it. SSH usernames are `ubuntu`\n\non Linux and `Administrator`\n\non Windows; VNC details exist only on `desktop_enabled`\n\nmachines.\n\n```\ncurl https://coasty.ai/v1/machines/$MACHINE_ID/connection \\\n  -H \"X-API-Key: $COASTY_API_KEY\"\n```\n\nScreenshots & snapshots. `GET /v1/machines/{id}/screenshot`\n\nreturns the current screen of a desktop machine (a still-booting VM returns `502 SCREENSHOT_FAILED`\n\n— poll for `running`\n\nfirst). `POST /v1/machines/{id}/snapshot`\n\ncaptures a restorable image (Linux), needs the `snapshots:write`\n\nscope, and is the one machine op with a flat fee (`$0.01`\n\nper snapshot). A conclusive pre-creation failure is refunded, confirmed by `X-Credits-Refunded`\n\n. A timeout, an upstream 5xx, or malformed post-dispatch result may already have created the image and instead returns terminal `503 SNAPSHOT_OUTCOME_UNKNOWN`\n\nwithout a blind refund or re-execution. Boot a future machine from a confirmed snapshot with `restore_from_snapshot: true`\n\n.\n\nThe control surface. Drive the VM directly with these endpoints. Each enforces a specific scope, and the high-risk ones — `browser_execute`\n\n(arbitrary JS) and anything under `connection:read`\n\n— are opt-in. `/actions/batch`\n\nruns up to 50 steps and stops on the first error by default (`stop_on_error: false`\n\nto continue); `/terminal`\n\ntruncates output to 5000 chars.\n\n```\ncurl -X POST https://coasty.ai/v1/machines/$MACHINE_ID/terminal \\\n  -H \"X-API-Key: $COASTY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"command\": \"ls -la /home/ubuntu\"}'\n```\n\nIdempotency & safety. The machine operations in the exact reserve-and-replay set — provision, snapshot, actions, action batches, browser, terminal, and file operations — accept an `Idempotency-Key`\n\n(≤128 chars). Lifecycle start/stop/restart, TTL updates, and termination do not. For supported operations, a duplicate key replays the original result without re-executing; a duplicate that arrives while the first is still running waits up to ~25s, then returns `409 IDEMPOTENCY_IN_FLIGHT`\n\n. One nuance worth knowing: a command that never reached the VM (a dispatch failure) is *not* cached, so retrying the same key runs it fresh; a command the VM actually ran (even if it errored) *is* cached. Every machine lookup is ownership-scoped, so a wrong or someone-else's id returns `404`\n\n— never a leak. Full code list in [Errors](#errors).\n\n## Predict\n\n`POST /v1/predict`\n\nis the low-level, stateless prediction primitive. Each call is independent: you provide the full context, execute the returned actions, capture the next screenshot, and decide when to call again. Use it when your application must own that loop. For an autonomous goal, start a [task run](#runs). When a manually driven loop needs server-side trajectory memory, reach for [sessions](#sessions) instead.\n\nThe response is the standard prediction shape, covered in [Response format](#responses).\n\n## Sessions\n\nA 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.\n\n``` python\nimport base64, os, requests\n\nBASE = \"https://coasty.ai/v1\"\nHEADERS = {\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]}\n\ndef screenshot() -> str:\n    with open(\"screen.png\", \"rb\") as f:\n        return base64.b64encode(f.read()).decode()\n\n# 1. Open a session — it remembers the trajectory across steps\nsession = requests.post(f\"{BASE}/sessions\", headers=HEADERS, json={\n    \"screen_width\": 1920,\n    \"screen_height\": 1080,\n}, timeout=60).json()\nsession_id = session[\"session_id\"]\n\n# 2. Drive the task one step at a time\ntry:\n    for _ in range(20):  # safety cap\n        res = requests.post(\n            f\"{BASE}/sessions/{session_id}/predict\",\n            headers=HEADERS,\n            json={\n                \"screenshot\": screenshot(),\n                \"instruction\": \"Book a meeting tomorrow at 3pm\",\n            },\n            timeout=60,\n        ).json()\n\n        for action in res[\"actions\"]:\n            perform(action)          # your action executor\n\n        if res[\"status\"] != \"continue\":\n            break\nfinally:\n    # 3. Always release the session to free your concurrency quota\n    requests.delete(f\"{BASE}/sessions/{session_id}\", headers=HEADERS, timeout=30)\n```\n\n`finally`\n\nblock. Sessions count against your tier's concurrent-session limit. The 2-hour (7200-second) idle TTL is reset on each predict and reset; deleting the session releases the slot immediately.## Grounding\n\nGrounding answers a narrower question than predict: “where is this element?” Give it a screenshot and a description and it returns the exact `x`\n\n, `y`\n\ncoordinate to target. It is faster and cheaper than a full prediction ($0.03 instead of $0.05), which makes it ideal when you already know what to do and only need a pixel to click.\n\n``` python\nimport os, requests\n\nres = requests.post(\n    \"https://coasty.ai/v1/ground\",\n    headers={\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]},\n    json={\n        \"screenshot\": screenshot,   # base64 PNG (see Predict)\n        \"element\": \"the blue Submit button below the form\",\n    },\n    timeout=60,\n).json()\n\nprint(res[\"x\"], res[\"y\"])           # exact click coordinates\n```\n\nThe response is `{ x, y, usage, request_id }`\n\n. Coordinates are in the same pixel space as the screenshot you sent.\n\n## Parse\n\nParse converts a block of `pyautogui`\n\ncode 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.\n\n``` python\nimport os, requests\n\nres = requests.post(\n    \"https://coasty.ai/v1/parse\",\n    headers={\"X-API-Key\": os.environ[\"COASTY_API_KEY\"]},\n    json={\"code\": \"pyautogui.click(100, 200)\\npyautogui.typewrite('hello')\"},\n    timeout=30,\n).json()\n\nfor action in res[\"actions\"]:\n    print(action[\"action_type\"], action[\"params\"])\n```\n\n## Action types\n\nEvery action the model can return uses an `action_type`\n\nfrom the table below, paired with a `params`\n\nobject. Your executor switches on the type and applies the parameters. The terminal types — `done`\n\nand `fail`\n\n— set the response `status`\n\nand signal you to stop looping.\n\n## Response format\n\nPredict and session-predict return the same shape. `actions`\n\nis the ordered list to execute; `status`\n\ntells you whether to keep going (`continue`\n\n), stop successfully (`done`\n\n), or stop because the task is impossible (`fail`\n\n). `usage`\n\nreports tokens and the dollar cost of the call (`cost_cents`\n\n).\n\nBilled success responses also carry two headers you can read without parsing the body: `X-Credits-Charged`\n\n(what this call cost) and `X-Credits-Remaining`\n\n(your wallet balance after it). In the body, the same numbers appear as `usage.credits_charged`\n\nand `usage.cost_cents`\n\n. On an `sk-coasty-test-`\n\nkey both are always `0`\n\n. Every response (success or error) additionally carries an `X-Coasty-Request-Id`\n\nheader that mirrors `request_id`\n\n; quote it when contacting support.\n\nOn a failed billed request, `X-Credits-Refunded`\n\nis the authoritative confirmation that credits were returned, and `X-Credits-Charged`\n\nis then`0`\n\n. If settlement cannot be confirmed, the API returns`503 BILLING_UNAVAILABLE`\n\nwithout the refund header. Follow the error's`retry_with_same_idempotency_key`\n\nfield rather than retrying by status alone.\n\n```\n{\n  \"request_id\": \"req_8f2c1e9a\",\n  \"status\": \"continue\",\n  \"reasoning\": \"The login form is visible. I'll click the email field, then type the address.\",\n  \"actions\": [\n    {\n      \"action_type\": \"click\",\n      \"params\": {\n        \"x\": 512,\n        \"y\": 340\n      },\n      \"description\": \"Click the email field\"\n    },\n    {\n      \"action_type\": \"type_text\",\n      \"params\": {\n        \"text\": \"[email protected]\"\n      },\n      \"description\": \"Type the email address\"\n    }\n  ],\n  \"raw_code\": [\n    \"pyautogui.click(512, 340)\",\n    \"pyautogui.typewrite('[email protected]')\"\n  ],\n  \"usage\": {\n    \"input_tokens\": 1523,\n    \"output_tokens\": 245,\n    \"credits_charged\": 5,\n    \"cost_cents\": 5\n  }\n}\n```\n\n## Errors\n\nErrors return a non-2xx status and a JSON envelope under an `error`\n\nkey. The `code`\n\nis stable and safe to branch on; `message`\n\nis human-readable and may change. Every error also carries an `error.request_id`\n\n(mirrored in the `X-Coasty-Request-Id`\n\nresponse header), plus `error.suggestion`\n\nand `error.docs_url`\n\nfor self-service. A `Link: <url>; rel=\"help\"`\n\nheader mirrors `docs_url`\n\n. Always log the request id: it is the fastest way for us to trace a failed call.\n\nSome codes attach machine-readable context to the body. A `402`\n\n(`INSUFFICIENT_CREDITS`\n\n) reports `required`\n\nand `balance`\n\n; a `403`\n\nreports `required_scope`\n\nand `current_scopes`\n\n; a `422`\n\n`VALIDATION_ERROR`\n\nlists the offending field path under `error.details`\n\n; and a `409`\n\nstate conflict carries `current_state`\n\nwith `allowed_from`\n\nor `required_state`\n\n.\n\n```\n{\n  \"error\": {\n    \"code\": \"INSUFFICIENT_CREDITS\",\n    \"message\": \"Your API wallet does not have enough funds to complete this request.\",\n    \"type\": \"billing_error\",\n    \"suggestion\": \"Add funds in the dashboard, or use an sk-coasty-test- key while building (test keys never bill).\",\n    \"docs_url\": \"https://coasty.ai/docs#errors\",\n    \"required\": 5,\n    \"balance\": 2,\n    \"request_id\": \"req_8f2c1e9a\",\n    \"retryable\": false,\n    \"retry_with_same_idempotency_key\": false\n  }\n}\n```\n\n`429`\n\n, `503`\n\n(`UPSTREAM_UNAVAILABLE`\n\n), and `504`\n\n(`UPSTREAM_TIMEOUT`\n\n) as retryable: honor `Retry-After`\n\non a 429, and use an `Idempotency-Key`\n\nwith exponential backoff on the upstream codes only when the response's `retryable`\n\nand`retry_with_same_idempotency_key`\n\nfields permit it. A`500`\n\nmodel failure (`PREDICTION_FAILED`\n\nor `GROUNDING_FAILED`\n\n) is refunded only when `X-Credits-Refunded`\n\nis present; after a confirmed refund, use a new idempotency key for a new execution.### Troubleshooting\n\nFive mistakes account for almost every first-week support ticket. Each maps to one status and one fix:\n\n## Pricing\n\nRequests are billed in US dollars from your prepaid API wallet. The charge is taken before the model runs. On a conclusive server-side failure, the API submits a refund; treat it as complete only when `X-Credits-Refunded`\n\nis present. Ambiguous provider mutations may retain the debit until reconciliation. Internally each request unit is `$0.01`\n\n(the granularity behind every price below), but everything you pay and see is dollars. Every price on this page is exact; test keys (`sk-coasty-test-`\n\n) always bill `$0.00`\n\n.\n\n### Surcharges\n\nFour fixed surcharges can apply on top of a base price, all on the vision endpoints (predict, session steps, ground). Each is an exact USD amount:\n\n### Machines\n\nMachines bill for runtime only, metered per minute and rounded down: `$0.05/hr`\n\nfor a running Linux machine, `$0.09/hr`\n\nfor a running Windows machine, and `$0.01/hr`\n\nwhile stopped or suspended. The starting, stopping, and restarting transitions bill at the running rate; the creating, error, and terminated states bill nothing, and TTL auto-destroy is free. Snapshots are a one-time `$0.01`\n\neach, and every per-call operation (actions, batch, browser, terminal, files, screenshot, connection) is free. Provisioning requires a `$0.20`\n\nwallet minimum, which is a gate, not a charge. If the wallet empties mid-flight the machine is automatically stopped, never destroyed, and resumes after you top up. The live rate card is always at `GET /v1/machines/pricing`\n\n.\n\n### Schedules\n\nSchedules have no per-fire fee: webhook fires are free (limited to 60/min), and create, run-now, and webhook fires only require the same `$0.20`\n\nwallet minimum as a gate. The execution itself is billed differently from everything else on this page: scheduled agent runtime is charged to your subscription credit balance at 10 credits per minute ($0.10 of subscription value per minute, at 1 credit = $0.01), not to this USD API wallet. Keep both balances funded if you rely on schedules.\n\n## Cookbook\n\nCurated open-source repositories for building on the Coasty API. Clone a repo, mint a key, and ship.\n\n[coasty-ai/computer-use-cookbookExamplesPythonNodecURLView on GitHub](https://github.com/coasty-ai/computer-use-cookbook)\n\n### Computer Use Cookbook\n\nRunnable, copy-paste examples for every part of the Coasty API: predict, sessions, task runs, workflows, and driving machines. Start here.\n\n[coasty-ai/open-coworkOpen sourceAgentsReference appView on GitHub](https://github.com/coasty-ai/open-cowork)\n\n### Open Cowork\n\nThe open-source Coasty project for computer-use agents that work alongside you. Build on it, fork it, or contribute.", "url": "https://wpnews.pro/news/launch-hn-coasty-yc-s26-an-api-for-computer-use-agents", "canonical_source": "https://coasty.ai/docs", "published_at": "2026-07-15 15:51:20+00:00", "updated_at": "2026-07-15 17:13:39.511668+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure", "ai-products"], "entities": ["Coasty", "YC"], "alternates": {"html": "https://wpnews.pro/news/launch-hn-coasty-yc-s26-an-api-for-computer-use-agents", "markdown": "https://wpnews.pro/news/launch-hn-coasty-yc-s26-an-api-for-computer-use-agents.md", "text": "https://wpnews.pro/news/launch-hn-coasty-yc-s26-an-api-for-computer-use-agents.txt", "jsonld": "https://wpnews.pro/news/launch-hn-coasty-yc-s26-an-api-for-computer-use-agents.jsonld"}}