> ## Documentation Index
> Fetch the complete documentation index at: https://docs.guidinghand.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a task on a computer

> On your customer's computer: send them an invite link, run a task on their Mac or PC while they watch, and read the result.

Most integrations [talk to an agent](https://docs.guidinghand.ai/quickstart.md), and the agent starts tasks on its own browser and computers when it needs them. This page is the other way in: you start a task yourself, on a computer someone connected for you. It's how an agent works [on your customer's computer](https://guidinghand.ai/use-cases/customer-computer): a customer opens an invite link, connects their Mac or PC, and watches the agent fix the problem on their screen, answering its questions and approving its steps as it goes.

It walks through one complete task with the `/v1` API: a session for a customer, their computer connecting, a task that asks a question and an approval, and the finished result with its replay. Every step has cURL, Node and Python. The Node code needs Node 18 or later (it uses the built-in `fetch`). The Python code needs `requests` (`pip install requests`). A single script with all of it is at the [end of the page](#the-whole-thing).

> [!TIP]
>
> With an SDK the whole flow is about 15 lines: `npm install guidinghand` ([JavaScript](https://docs.guidinghand.ai/sdks/javascript.md)) or `pip install guidinghand` ([Python](https://docs.guidinghand.ai/sdks/python.md)). `tasks.run()` starts a task and answers its questions and approvals with your callbacks. This page shows the HTTP underneath.

**1. Get an API key**

Sign in at [guidinghand.ai/console](https://guidinghand.ai/console) and open **Settings → API**. Create a key and copy it: it starts with `gh_live_` and is shown once. Creating keys needs the admin or owner role in the org.

```bash theme={null}
export EAGER_API_KEY="gh_live_..."
```

Every request sends it as a bearer token. The Node and Python snippets below share this small helper, which turns error responses into exceptions:

```bash cURL theme={null}
# Each cURL example sends these headers:
#   -H "Authorization: Bearer $EAGER_API_KEY"
#   -H "Content-Type: application/json"   (when there is a body)
curl https://guidinghand.ai/v1/agents \
  -H "Authorization: Bearer $EAGER_API_KEY"
```

```javascript Node theme={null}
const BASE = 'https://guidinghand.ai';
const KEY = process.env.EAGER_API_KEY;

async function gh(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw Object.assign(new Error(`${res.status} ${data.error.type}: ${data.error.message}`), { code: data.error.code });
  return data;
}
```

```python Python theme={null}
import os
import requests

BASE = "https://guidinghand.ai"
http = requests.Session()
http.headers["Authorization"] = f"Bearer {os.environ['EAGER_API_KEY']}"


def gh(method, path, body=None, params=None):
    r = http.request(method, BASE + path, json=body, params=params, timeout=70)
    data = r.json()
    if not r.ok:
        err = RuntimeError(f"{r.status_code} {data['error']['type']}: {data['error']['message']}")
        err.code = data["error"].get("code")  # e.g. "already_decided"
        raise err
    return data
```

> [!TIP]
>
> To try things without touching production billing, use `https://dev.guidinghand.ai` as the base URL with a key made in the dev console. Dev runs Stripe in test mode.

**2. Create an agent (optional)**

An agent holds your instructions, the model it runs on, its reasoning effort and the greeting the customer sees on the invite page. You can skip this step: a session that names no agent runs on Eager's own instructions alone.

```bash cURL theme={null}
curl -X POST https://guidinghand.ai/v1/agents \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "billing",
    "name": "Billing help",
    "instructions": "Our billing app is Acme Billing. Open it from the Dock before anything else.",
    "effort": "medium",
    "greeting": "Hi, this is Acme support. We will fix your billing settings together."
  }'
```

```javascript Node theme={null}
const agent = await gh('POST', '/v1/agents', {
  agent_id: 'billing',
  name: 'Billing help',
  instructions: 'Our billing app is Acme Billing. Open it from the Dock before anything else.',
  effort: 'medium',
  greeting: 'Hi, this is Acme support. We will fix your billing settings together.',
});
console.log(agent.invite_url_template);
```

```python Python theme={null}
agent = gh("POST", "/v1/agents", {
    "agent_id": "billing",
    "name": "Billing help",
    "instructions": "Our billing app is Acme Billing. Open it from the Dock before anything else.",
    "effort": "medium",
    "greeting": "Hi, this is Acme support. We will fix your billing settings together.",
})
print(agent["invite_url_template"])
```

`agent_id` becomes part of invite links, so it is lowercase letters, numbers and dashes. Creating an agent needs the admin role; an API key has it. An `agent_id` that already exists returns `409 conflict`. `model` sets the price of the agent's tokens. Without a `model`, the agent runs on Eager's default, GPT-6 Astra: \$15 per million input tokens and \$75 per million output, about 58¢ for a typical 10-step task. Claude Sonnet 5.5 (`"model": "claude-sonnet-5-5"`) is \$3 and \$15, about 15¢ a task. `effort` has no price of its own: `medium` thinks more each step, so it uses more tokens. See [Agents](https://docs.guidinghand.ai/concepts/agents.md#model).

**3. Create a session and send the invite link**

A session is one pairing code for one computer. Put your own ids in `metadata` so you can match the session to a ticket later.

```bash cURL theme={null}
curl -X POST https://guidinghand.ai/v1/sessions \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "billing", "metadata": {"ticket_id": "T-1042"}}'
```

```javascript Node theme={null}
const session = await gh('POST', '/v1/sessions', {
  agent_id: 'billing',
  metadata: { ticket_id: 'T-1042' },
});
console.log(`Send this link to the customer: ${session.invite_url}`);
```

```python Python theme={null}
session = gh("POST", "/v1/sessions", {"agent_id": "billing", "metadata": {"ticket_id": "T-1042"}})
print("Send this link to the customer:", session["invite_url"])
```

```json Response theme={null}
{
  "object": "session",
  "session_id": "K7QM-24XP",
  "code": "K7QM-24XP",
  "agent_id": "billing",
  "invite_url": "https://guidinghand.ai/acme/billing/K7QM-24XP",
  "status": "waiting",
  "device": null,
  "metadata": { "ticket_id": "T-1042" },
  "created_at": "2026-09-26T15:03:36.042Z",
  "paired_at": null,
  "last_active_at": "2026-09-26T15:03:36.042Z",
  "expires_at": "2026-09-29T15:03:36.042Z",
  "task_count": 0,
  "session_token": "gh_sk_..."
}
```

Send `invite_url` to the customer through whatever channel you already use: chat, email or the ticket. The page walks them through installing the desktop app, granting the macOS permissions, and clicking **Connect**. The `session_id` is the code itself.

`session_token` is for the older [session-token API](https://docs.guidinghand.ai/api-reference/introduction.md#the-older-session-token-api). It is shown once; you don't need it for `/v1`.

**4. Wait until the computer is connected**

Tasks need `status: connected`. Either poll the session, or set up a [webhook](https://docs.guidinghand.ai/guides/webhooks.md) and wait for `session.connected`.

```bash cURL theme={null}
curl https://guidinghand.ai/v1/sessions/K7QM-24XP \
  -H "Authorization: Bearer $EAGER_API_KEY"
```

```javascript Node theme={null}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

let s = session;
while (s.status !== 'connected') {
  if (s.status === 'expired') throw new Error('The code expired before anyone connected.');
  await sleep(3000);
  s = await gh('GET', `/v1/sessions/${session.session_id}`);
}
console.log(`Connected: ${s.device?.name ?? s.device?.os}`);
```

```python Python theme={null}
import time

s = session
while s["status"] != "connected":
    if s["status"] == "expired":
        raise RuntimeError("The code expired before anyone connected.")
    time.sleep(3)
    s = gh("GET", f"/v1/sessions/{session['session_id']}")
print("Connected:", (s["device"] or {}).get("name") or (s["device"] or {}).get("os"))
```

Once connected, `device` tells you the operating system (`mac` or `windows`), the computer's name and its screen size.

**5. Start a task**

Describe the outcome in plain language. `request_id` is an idempotency key: if your request times out and you send it again with the same `request_id`, you get the same task back instead of a second one.

```bash cURL theme={null}
curl -X POST https://guidinghand.ai/v1/sessions/K7QM-24XP/tasks \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Turn on Dark Mode", "request_id": "T-1042-dark-mode"}'
```

```javascript Node theme={null}
let task = await gh('POST', `/v1/sessions/${session.session_id}/tasks`, {
  prompt: 'Turn on Dark Mode',
  request_id: 'T-1042-dark-mode',
});
console.log(task.task_id, task.status);
```

```python Python theme={null}
task = gh("POST", f"/v1/sessions/{session['session_id']}/tasks", {
    "prompt": "Turn on Dark Mode",
    "request_id": "T-1042-dark-mode",
})
print(task["task_id"], task["status"])
```

```json Response theme={null}
{
  "object": "task",
  "task_id": "task_mW8mcFPUN7Of",
  "session_id": "K7QM-24XP",
  "agent_id": "billing",
  "status": "running",
  "done": false,
  "prompt": "Turn on Dark Mode",
  "result": null,
  "error": null,
  "pending": null,
  "cursor": 1,
  "metadata": {},
  "created_at": "2026-09-26T15:05:37.559Z",
  "updated_at": "2026-09-26T15:05:37.562Z",
  "active_seconds": 0,
  "model": "gpt-6-astra",
  "effort": "medium",
  "usage": null,
  "cost_cents": null,
  "replay_url": "https://guidinghand.ai/console/acme/tasks/task_mW8mcFPUN7Of"
}
```

One task runs at a time per computer. Starting another while one runs returns `409 conflict` with the running task's id in `error.active_task_id`.

**6. Follow the task and answer what it asks**

`GET /v1/tasks/{task_id}/events` returns the events after `after`. With `wait_ms` (up to 55000) it holds the request open until something new happens, so a simple loop gets every event as it happens. Pass the returned `cursor` as `after` on the next call, and stop when `task.done` is `true`.

When the agent needs a person, it asks in the task's [conversation](https://docs.guidinghand.ai/concepts/tasks.md#questions-and-approvals) (`task.conversation_id`): the task is `paused`, `task.pending` holds the request, and you decide it there, with `POST /v1/conversations/{conversation_id}/decisions`:

* `pending.type: "question"`: send `answer` with its `question_id` and your `answer`. The customer usually sees the question on their screen too (`pending.customer_can_answer`) and can answer it there. The first answer is used; answering after them returns `409` with `code: "already_decided"` (or `"not_pending"` once the task has ended), which just means it's done.
* `pending.type: "approval"`: send `approval` with its `approval_id` and a `decision` of `approve` or `deny`. The customer usually sees the request on their screen too (`pending.customer_can_approve`) and can allow it or not there. As with questions, the first decision is used, and `already_decided` or `not_pending` just means it's done.

```bash cURL theme={null}
# Follow: repeat with after=<cursor from the last response>
curl "https://guidinghand.ai/v1/tasks/task_mW8mcFPUN7Of/events?after=0&wait_ms=25000" \
  -H "Authorization: Bearer $EAGER_API_KEY"

# Answer a question, in the task's conversation (task.conversation_id)
curl -X POST https://guidinghand.ai/v1/conversations/conv_4kT9mQ2xLp7e/decisions \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"answer": {"question_id": "qst_yr7jammy2zmmq3w2kx4c", "answer": "Work"}}'

# Decide an approval
curl -X POST https://guidinghand.ai/v1/conversations/conv_4kT9mQ2xLp7e/decisions \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"approval": {"approval_id": "apr_oztpjvlnbzk3w2qxsmfa", "decision": "approve"}}'
```

```javascript Node theme={null}
import readline from 'node:readline/promises';
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

let after = 0;
const answered = new Set();
while (!task.done) {
  const page = await gh('GET', `/v1/tasks/${task.task_id}/events?after=${after}&wait_ms=25000`);
  for (const e of page.data) console.log(`[${e.type}] ${e.message ?? ''}`);
  after = page.cursor;
  task = page.task;

  const p = task.pending;
  if (p && !answered.has(p.question_id ?? p.approval_id)) {
    if (p.type === 'question') {
      const answer = await rl.question(`${p.question} > `);
      await gh('POST', `/v1/conversations/${task.conversation_id}/decisions`, { answer: { question_id: p.question_id, answer } })
        .catch((e) => { if (!['already_decided', 'not_pending'].includes(e.code)) throw e; }); // answered on their screen first, or already closed
      answered.add(p.question_id);
    } else {
      const where = p.customer_can_approve ? ' (they can decide on their screen too)' : '';
      const yes = await rl.question(`Approve "${p.action}"${where}? [y/N] > `);
      await gh('POST', `/v1/conversations/${task.conversation_id}/decisions`, { approval: {
        approval_id: p.approval_id,
        decision: yes.trim().toLowerCase() === 'y' ? 'approve' : 'deny',
      } }).catch((e) => { if (!['already_decided', 'not_pending'].includes(e.code)) throw e; }); // decided on their screen first, or already closed
      answered.add(p.approval_id);
    }
  }
}
rl.close();
```

```python Python theme={null}
after = 0
answered = set()
while not task["done"]:
    page = gh("GET", f"/v1/tasks/{task['task_id']}/events", params={"after": after, "wait_ms": 25000})
    for e in page["data"]:
        print(f"[{e['type']}] {e.get('message', '')}")
    after = page["cursor"]
    task = page["task"]

    p = task["pending"]
    if p and (p.get("question_id") or p.get("approval_id")) not in answered:
        if p["type"] == "question":
            answer = input(f"{p['question']} > ")
            try:
                gh("POST", f"/v1/conversations/{task['conversation_id']}/decisions", {"answer": {"question_id": p["question_id"], "answer": answer}})
            except RuntimeError as e:
                if getattr(e, "code", None) not in ("already_decided", "not_pending"):  # answered on their screen first, or already closed
                    raise
            answered.add(p["question_id"])
        else:
            where = " (they can decide on their screen too)" if p.get("customer_can_approve") else ""
            yes = input(f"Approve \"{p['action']}\"{where}? [y/N] > ")
            try:
                gh("POST", f"/v1/conversations/{task['conversation_id']}/decisions", {"approval": {
                    "approval_id": p["approval_id"],
                    "decision": "approve" if yes.strip().lower() == "y" else "deny",
                }})
            except RuntimeError as e:
                if getattr(e, "code", None) not in ("already_decided", "not_pending"):  # decided on their screen first, or already closed
                    raise
            answered.add(p["approval_id"])
```

A response while the agent waits on a question looks like this:

```json Response theme={null}
{
  "data": [
    { "cursor": 1, "type": "started", "message": "Task received. Starting on the connected computer…", "ts": "2026-09-26T15:05:37.561Z" },
    { "cursor": 2, "type": "progress", "message": "Connected to the computer. Looking at the screen…", "ts": "2026-09-26T15:05:37.569Z" },
    { "cursor": 3, "type": "action", "message": "Click “Search”", "ts": "2026-09-26T15:05:39.584Z",
      "data": { "action": { "type": "click", "button": "left", "x": 412, "y": 88 } } },
    { "cursor": 4, "type": "question", "message": "Which account?", "ts": "2026-09-26T15:05:41.593Z",
      "data": { "question_id": "qst_yr7jammy2zmmq3w2kx4c", "question": "Which account?", "options": ["Work", "Home"] } }
  ],
  "cursor": 4,
  "task": {
    "object": "task",
    "task_id": "task_mW8mcFPUN7Of",
    "session_id": "K7QM-24XP",
    "agent_id": "billing",
    "status": "paused",
    "done": false,
    "prompt": "Turn on Dark Mode",
    "result": null,
    "error": null,
    "conversation_id": "conv_4kT9mQ2xLp7e",
    "paused_for": { "kind": "question", "id": "qst_yr7jammy2zmmq3w2kx4c", "conversation_id": "conv_4kT9mQ2xLp7e" },
    "pending": {
      "type": "question",
      "question_id": "qst_yr7jammy2zmmq3w2kx4c",
      "question": "Which account?",
      "options": ["Work", "Home"],
      "customer_can_answer": true,
      "conversation_id": "conv_4kT9mQ2xLp7e"
    },
    "cursor": 4,
    "metadata": {},
    "created_at": "2026-09-26T15:05:37.559Z",
    "updated_at": "2026-09-26T15:05:41.593Z",
    "active_seconds": 4,
    "model": "gpt-6-astra",
    "effort": "medium",
    "usage": { "input_tokens": 31840, "cached_tokens": 14336, "cache_write_tokens": 0, "output_tokens": 412, "reasoning_tokens": 128 },
    "cost_cents": null,
    "replay_url": "https://guidinghand.ai/console/acme/tasks/task_mW8mcFPUN7Of"
  }
}
```

In a real integration the question usually goes to your support agent's screen or to your own AI agent, not to a terminal prompt, or you leave it to the customer, who can answer it on their screen. `answered` keeps the loop from answering the same question twice if the next page arrives before the task moves on.

> [!NOTE]
>
> A task waiting on a question or approval uses no tokens, so the wait costs nothing. A task pays for the tokens its model uses, at the price of the agent's model (GPT-6 Astra is \$15 per million input tokens and \$75 per million output). See [Billing](https://docs.guidinghand.ai/concepts/billing.md).

**7. Read the result**

When `task.done` is `true`, `status` is `completed`, `failed` or `stopped`. A completed task has the agent's summary in `result`. A failed task has the reason in `error`. For a stopped task, the reason is the message of its last event (`stopped`), for example "Stopped by the person at the computer."

```bash cURL theme={null}
curl https://guidinghand.ai/v1/tasks/task_mW8mcFPUN7Of \
  -H "Authorization: Bearer $EAGER_API_KEY"
```

```javascript Node theme={null}
console.log(`${task.status}: ${task.result ?? task.error ?? ''}`);
```

```python Python theme={null}
print(f"{task['status']}: {task['result'] or task['error'] or ''}")
```

```json Response theme={null}
{
  "object": "task",
  "task_id": "task_mW8mcFPUN7Of",
  "session_id": "K7QM-24XP",
  "agent_id": "billing",
  "status": "completed",
  "done": true,
  "prompt": "Turn on Dark Mode",
  "result": "Dark Mode is on. I switched Appearance to Dark in System Settings.",
  "error": null,
  "pending": null,
  "cursor": 14,
  "metadata": {},
  "created_at": "2026-09-26T15:05:37.559Z",
  "updated_at": "2026-09-26T15:06:52.106Z",
  "active_seconds": 41,
  "model": "gpt-6-astra",
  "effort": "medium",
  "usage": { "input_tokens": 98460, "cached_tokens": 86016, "cache_write_tokens": 0, "output_tokens": 1380, "reasoning_tokens": 452 },
  "cost_cents": 42,
  "replay_url": "https://guidinghand.ai/console/acme/tasks/task_mW8mcFPUN7Of",
  "recording": { "frames": 9 }
}
```

Add `?include=events` to get every event with the task.

**8. Open the replay**

`replay_url` opens the task in the console: every screen the agent saw, with its cursor and clicks, next to the timeline of events. Anyone in the org can open it after signing in.

To get the screens yourself, `GET /v1/tasks/{task_id}/recording` lists the frames and each frame's `url` returns the PNG. See [Recordings](https://docs.guidinghand.ai/concepts/recordings.md).

```bash cURL theme={null}
curl https://guidinghand.ai/v1/tasks/task_mW8mcFPUN7Of/recording \
  -H "Authorization: Bearer $EAGER_API_KEY"

curl https://guidinghand.ai/v1/tasks/task_mW8mcFPUN7Of/recording/1 \
  -H "Authorization: Bearer $EAGER_API_KEY" \
  -o frame-1.png
```

```javascript Node theme={null}
console.log(`Replay: ${task.replay_url}`);
const rec = await gh('GET', `/v1/tasks/${task.task_id}/recording`);
console.log(`${rec.frames.length} frames`);
```

```python Python theme={null}
print("Replay:", task["replay_url"])
rec = gh("GET", f"/v1/tasks/{task['task_id']}/recording")
print(len(rec["frames"]), "frames")
```

## The whole thing

Save one of these and run it with `EAGER_API_KEY` set. It makes a `support` agent (once), prints the invite link, waits for the computer, runs one task and answers questions and approvals from your terminal (the customer can also answer the questions on their screen). Set `EAGER_API_URL=https://dev.guidinghand.ai` to run it against dev.

```javascript quickstart.mjs theme={null}
// node quickstart.mjs (Node 18 or later)
import readline from 'node:readline/promises';

const BASE = process.env.EAGER_API_URL ?? 'https://guidinghand.ai';
const KEY = process.env.EAGER_API_KEY;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function gh(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw Object.assign(new Error(`${res.status} ${data.error.type}: ${data.error.message}`), { code: data.error.code });
  return data;
}

// 1. An agent (409 means an earlier run made it), a session, and the invite link for the customer
await gh('POST', '/v1/agents', { agent_id: 'support', name: 'Support' })
  .catch((e) => { if (!e.message.startsWith('409')) throw e; });
const session = await gh('POST', '/v1/sessions', {
  agent_id: 'support',
  metadata: { ticket_id: 'T-1042' },
});
console.log(`Send this link to the customer: ${session.invite_url}`);

// 2. Wait for their computer to connect
let s = session;
while (s.status !== 'connected') {
  if (s.status === 'expired') throw new Error('The code expired before anyone connected.');
  await sleep(3000);
  s = await gh('GET', `/v1/sessions/${session.session_id}`);
}
console.log(`Connected: ${s.device?.name ?? s.device?.os}`);

// 3. Start a task
let task = await gh('POST', `/v1/sessions/${session.session_id}/tasks`, {
  prompt: 'Turn on Dark Mode',
  request_id: `T-1042-dark-mode`,
});

// 4. Follow it, answering questions and approvals
let after = 0;
const answered = new Set();
while (!task.done) {
  const page = await gh('GET', `/v1/tasks/${task.task_id}/events?after=${after}&wait_ms=25000`);
  for (const e of page.data) console.log(`[${e.type}] ${e.message ?? ''}`);
  after = page.cursor;
  task = page.task;

  const p = task.pending;
  if (p && !answered.has(p.question_id ?? p.approval_id)) {
    if (p.type === 'question') {
      const hint = p.options.length ? ` (${p.options.join(' / ')})` : '';
      const where = p.customer_can_answer ? ' (they can answer on their screen too)' : '';
      const answer = await rl.question(`${p.question}${hint}${where} > `);
      await gh('POST', `/v1/conversations/${task.conversation_id}/decisions`, { answer: { question_id: p.question_id, answer } })
        .catch((e) => { if (!['already_decided', 'not_pending'].includes(e.code)) throw e; }); // answered on their screen first, or already closed
      answered.add(p.question_id);
    } else {
      const where = p.customer_can_approve ? ' (they can decide on their screen too)' : '';
      const yes = await rl.question(`Approve "${p.action}" (risk: ${p.risk})${where}? [y/N] > `);
      await gh('POST', `/v1/conversations/${task.conversation_id}/decisions`, { approval: {
        approval_id: p.approval_id,
        decision: yes.trim().toLowerCase() === 'y' ? 'approve' : 'deny',
      } }).catch((e) => { if (!['already_decided', 'not_pending'].includes(e.code)) throw e; }); // decided on their screen first, or already closed
      answered.add(p.approval_id);
    }
  }
}

// 5. The result and the replay
console.log(`${task.status}: ${task.result ?? task.error ?? ''}`);
console.log(`Replay: ${task.replay_url}`);
rl.close();
```

```python quickstart.py theme={null}
# pip install requests && python quickstart.py
import os
import time

import requests

BASE = os.environ.get("EAGER_API_URL", "https://guidinghand.ai")
http = requests.Session()
http.headers["Authorization"] = f"Bearer {os.environ['EAGER_API_KEY']}"


def gh(method, path, body=None, params=None):
    r = http.request(method, BASE + path, json=body, params=params, timeout=70)
    data = r.json()
    if not r.ok:
        err = RuntimeError(f"{r.status_code} {data['error']['type']}: {data['error']['message']}")
        err.code = data["error"].get("code")  # e.g. "already_decided"
        raise err
    return data


# 1. An agent (409 means an earlier run made it), a session, and the invite link for the customer
try:
    gh("POST", "/v1/agents", {"agent_id": "support", "name": "Support"})
except RuntimeError as e:
    if not str(e).startswith("409"):
        raise
session = gh("POST", "/v1/sessions", {"agent_id": "support", "metadata": {"ticket_id": "T-1042"}})
print("Send this link to the customer:", session["invite_url"])

# 2. Wait for their computer to connect
s = session
while s["status"] != "connected":
    if s["status"] == "expired":
        raise RuntimeError("The code expired before anyone connected.")
    time.sleep(3)
    s = gh("GET", f"/v1/sessions/{session['session_id']}")
print("Connected:", (s["device"] or {}).get("name") or (s["device"] or {}).get("os"))

# 3. Start a task
task = gh("POST", f"/v1/sessions/{session['session_id']}/tasks", {
    "prompt": "Turn on Dark Mode",
    "request_id": "T-1042-dark-mode",
})

# 4. Follow it, answering questions and approvals
after = 0
answered = set()
while not task["done"]:
    page = gh("GET", f"/v1/tasks/{task['task_id']}/events", params={"after": after, "wait_ms": 25000})
    for e in page["data"]:
        print(f"[{e['type']}] {e.get('message', '')}")
    after = page["cursor"]
    task = page["task"]

    p = task["pending"]
    if p and (p.get("question_id") or p.get("approval_id")) not in answered:
        if p["type"] == "question":
            hint = f" ({' / '.join(p['options'])})" if p["options"] else ""
            where = " (they can answer on their screen too)" if p.get("customer_can_answer") else ""
            answer = input(f"{p['question']}{hint}{where} > ")
            try:
                gh("POST", f"/v1/conversations/{task['conversation_id']}/decisions", {"answer": {"question_id": p["question_id"], "answer": answer}})
            except RuntimeError as e:
                if getattr(e, "code", None) not in ("already_decided", "not_pending"):  # answered on their screen first, or already closed
                    raise
            answered.add(p["question_id"])
        else:
            where = " (they can decide on their screen too)" if p.get("customer_can_approve") else ""
            yes = input(f"Approve \"{p['action']}\" (risk: {p['risk']}){where}? [y/N] > ")
            try:
                gh("POST", f"/v1/conversations/{task['conversation_id']}/decisions", {"approval": {
                    "approval_id": p["approval_id"],
                    "decision": "approve" if yes.strip().lower() == "y" else "deny",
                }})
            except RuntimeError as e:
                if getattr(e, "code", None) not in ("already_decided", "not_pending"):  # decided on their screen first, or already closed
                    raise
            answered.add(p["approval_id"])

# 5. The result and the replay
print(f"{task['status']}: {task['result'] or task['error'] or ''}")
print("Replay:", task["replay_url"])
```

## Next

* Replace polling with [webhooks](https://docs.guidinghand.ai/guides/webhooks.md).
* Let your own LLM agent drive Eager: [AI agents](https://docs.guidinghand.ai/guides/ai-agents.md).
* Handle [errors](https://docs.guidinghand.ai/guides/errors.md) such as a computer that went offline or an empty balance.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.