# End a recording early

Canonical page: https://agentsky.dev/tools/recall/bot-leave
JSON record: https://gateway.agentsky.dev/v1/endpoints/recall/bot-leave

End a recording early is a tool from Recall, callable through the AgentSky API as recall/bot-leave and billed per call ($0).

Requests that the recorder leave the meeting and stop capture. Usage remains pending until the supplier completes processing; bot time runs through done and transcription follows the recording lifecycle timestamps. Booking has no separate fee. A $2 prepayment is required; unused funds are returned at settlement. Usage is billed when the recording settles: $0.60 per hour the recorder holds a seat (from joining until supplier completion, including waiting and processing; fatal bot usage is waived) plus $0.18 per hour of recording lifecycle time, rounded to the nearest second without a minimum. Recording stops after two hours. A recorder that is never sent costs nothing; one that waits in a room nobody opens still costs the seat it held.

Confirmation that the recorder is leaving.

## Facts

- **Id:** `recall/bot-leave`
- **Provider:** Recall
- **Kind:** tool
- **Billed per:** call
- **Timeout:** 5 min
- **Max request:** 1 MB

## Call it

`POST https://gateway.agentsky.dev/v1/run` with `{"provider": "recall", "endpoint": "bot-leave", "input": {…}}` and `Authorization: Bearer $AGENTSKY_API_KEY`.

### Input fields

| Field | Type | Required | Description |
|---|---|---|---|
| `run_id` | string | yes | The gateway runId returned by bot-create. The booking must belong to this user and, for session tokens, this session. |
| `bot_id` | string | yes | The id returned when the recorder was booked. |

## Example

### curl

```bash
# Set once per job; reuse this key and the identical body after a timeout.
: "${AGENTSKY_IDEMPOTENCY_KEY:?Set a unique, saved key for this job}"
curl --fail-with-body https://gateway.agentsky.dev/v1/run \
  -H "authorization: Bearer $AGENTSKY_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: $AGENTSKY_IDEMPOTENCY_KEY" \
  -d '{"provider":"recall","endpoint":"bot-leave","input":{"run_id":"run_11111111222233334444555555555555","bot_id":"11111111-2222-3333-4444-555555555555"}}'
# If status is READY/RUNNING, poll GET /v1/runs/{runId} with the same API key.
```

### JavaScript

```js
// Node.js 20+. Save one unique key per job; reuse it with the identical body.
const key = process.env.AGENTSKY_IDEMPOTENCY_KEY;
if (!key || !process.env.AGENTSKY_API_KEY) {
  throw new Error("Set AGENTSKY_API_KEY and a saved AGENTSKY_IDEMPOTENCY_KEY first");
}
const base = "https://gateway.agentsky.dev";
const headers = {
  authorization: `Bearer ${process.env.AGENTSKY_API_KEY}`,
  "content-type": "application/json",
  "Idempotency-Key": key,
};
const body = JSON.stringify({
  "provider": "recall",
  "endpoint": "bot-leave",
  "input": {
    "run_id": "run_11111111222233334444555555555555",
    "bot_id": "11111111-2222-3333-4444-555555555555"
  }
});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function request(path, options = {}) {
  for (let attempt = 0; attempt < 3; attempt++) {
    let response;
    let data;
    try {
      response = await fetch(base + path, {
        ...options, headers, signal: AbortSignal.timeout(180_000),
      });
      const text = await response.text();
      try { data = JSON.parse(text); } catch { /* Proxy timeouts may return HTML. */ }
    } catch (error) {
      if (attempt === 2) throw error; // Resume with the same key and body.
      await sleep(2000);
      continue;
    }
    // Failed runs can have non-2xx HTTP status. Never regenerate them automatically.
    if (data?.runId && data?.status) return data;
    if (response.ok && data) throw new Error("Unexpected response: missing runId/status");
    if (attempt < 2 && (response.status >= 500 || response.status === 408 || response.status === 429)) {
      await sleep(2000);
      continue; // POST retries keep the original key and body.
    }
    throw new Error(`HTTP ${response.status}: ${JSON.stringify(data ?? "Non-JSON response; retry with the same key and body")}`);
  }
}

let run = await request("/v1/run", { method: "POST", body });
console.log("Save runId:", run.runId);
const deadline = Date.now() + 30 * 60_000;
while (["READY", "RUNNING"].includes(run.status)) {
  if (Date.now() >= deadline) {
    throw new Error(`Still pending: poll /v1/runs/${run.runId} later; do not create a new job`);
  }
  await sleep(5000);
  run = await request(`/v1/runs/${encodeURIComponent(run.runId)}`);
}
console.log("Status:", run.status, "Charge:", run.price?.amount);
if (run.status !== "COMPLETED") {
  throw new Error(`Run ${run.runId}: ${JSON.stringify(run.error ?? run.status)}`);
}
console.log(run.output); // Output shape depends on the endpoint.
// READY/RUNNING price is provisional. Use the terminal receipt for reconciliation.
```

## Pricing

$0 per call

Read the final customer charge from price.amount.value after the run finishes. A RUNNING response is not the final bill.

## Questions

### What does End a recording early do?

Requests that the recorder leave the meeting and stop capture. Usage remains pending until the supplier completes processing; bot time runs through done and transcription follows the recording lifecycle timestamps. Booking has no separate fee. A $2 prepayment is required; unused funds are returned at settlement. Usage is billed when the recording settles: $0.60 per hour the recorder holds a seat (from joining until supplier completion, including waiting and processing; fatal bot usage is waived) plus $0.18 per hour of recording lifecycle time, rounded to the nearest second without a minimum. Recording stops after two hours. A recorder that is never sent costs nothing; one that waits in a room nobody opens still costs the seat it held.

### How much does End a recording early cost?

$0 per call, using your plan when signed in. The customer charge is returned as price.amount.value with its currency; READY/RUNNING values are provisional.

### What does End a recording early take?

run_id and bot_id are required; the rest of the fields are optional. Send them in the input object of a POST to /v1/run.

### What are the limits on End a recording early?

The catalog lists an execution timeout of 5 min. A request can return READY or RUNNING before completion; poll GET /v1/runs/{runId} with the same API key until it reaches a terminal status. An HTTP timeout does not prove the generation stopped.

### How do I retry safely and check the final charge?

Save a unique Idempotency-Key for each job. After a network error or HTML timeout, resend the identical POST body with the same key and API key to recover the existing run, then poll its runId. A new key can start another billable generation. An HTTP error or timeout does not establish the run outcome or final charge. Pending runs can still settle; check the terminal price.amount and billing state, and retain the runId for reconciliation. Do not automatically create a new job after a terminal failure.
