---
title: Run a tool
description: Start a run, get results in one call or poll for them, page through large result sets, stop a run early, and see exactly what it cost.
---

A run sends your input to a tool, holds its maximum cost from your balance, and returns the results as JSON. Most runs finish in 5 to 60 seconds. You need an [API key](/app/keys) and enough balance to cover the run's maximum cost.

## Start a run

Send the tool's input and a result cap to `POST /v1/tools/{id}/run`:

```bash title="cURL"
curl https://lovelyrouter.com/v1/tools/google-maps-places/run \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "input": {
      "searchTerms": ["coffee shop"],
      "location": "Austin, Texas, USA",
      "language": "en"
    },
    "max_results": 20
  }'
```

:::params Body parameters
- `input` object, default `{}`
  The tool's input, matching its `input_schema` from [`GET /v1/tools/{id}`](/docs/tools/find#inspect-a-tool). Fields in `input_schema.required` must be set. Up to 50,000 characters as JSON.
- `max_results` integer
  The most results the run returns and bills. Defaults to the tool's `default_results`. Values above the tool's `max_results` are lowered to it. See [how the cap is set](#how-max-results-is-set).
- `wait` integer, default `60`
  Seconds to wait for the run to finish before answering, from 0 to 300. You can also pass it as the `?wait=` query parameter.
:::

The request waits for the run. What comes back depends on whether it finished within `wait` seconds:

| Status | Meaning |
| --- | --- |
| `200` | The run finished. The body has its final `status`, its charge and the results in `items`. |
| `202` | The run is still going. The body has its `id` and a `poll` path. [Poll it](#poll-a-long-run). |
| `4xx`, `5xx` | The run did not start, and nothing was charged. See [errors](#errors). |

## Read the result

A run that finishes in time returns `200`:

```json
{
  "id": "run_8c1f0e2a9b7d4f3e6a5c2b1d",
  "object": "tool_run",
  "tool": "google-maps-places",
  "tool_name": "Search Google Maps places",
  "status": "succeeded",
  "results": 20,
  "max_results": 20,
  "charge_usd": 0.00551,
  "reserved_usd": 0,
  "error": null,
  "created_at": "2026-10-11T15:02:11.204Z",
  "finished_at": "2026-10-11T15:02:38.917Z",
  "items": [
    {
      "title": "Godsent Coffee",
      "categories": ["Coffee shop"],
      "formattedAddress": "9817 Anderson Mill Rd, Austin, TX 78750",
      "website": "https://www.godsentcoffee.com/",
      "rating": 5,
      "reviewCount": 237,
      "placeId": "ChIJq7RZDGczW4YRyCS5Z0KiiDE",
      "googleMapsUri": "https://www.google.com/maps/search/?api=1&query=Godsent%20Coffee&query_place_id=ChIJq7RZDGczW4YRyCS5Z0KiiDE"
    }
  ],
  "offset": 0,
  "next_offset": null
}
```

A `200` means the run finished, not that it succeeded. Check `status`:

- `succeeded`: the run completed. `items` holds up to `max_results` results.
- `failed`, `timed_out` or `stopped`: the run ended early. `error` explains why, and `items` holds whatever it delivered before it ended. You pay only for those results.

Each item is one result in the tool's own format. Field names differ between tools.

## Poll a long run

A run still going after `wait` seconds comes back with `202`:

```json
{
  "id": "run_8c1f0e2a9b7d4f3e6a5c2b1d",
  "object": "tool_run",
  "tool": "google-maps-places",
  "tool_name": "Search Google Maps places",
  "status": "running",
  "results": 0,
  "max_results": 20,
  "charge_usd": null,
  "reserved_usd": 0.00551,
  "error": null,
  "created_at": "2026-10-11T15:02:11.204Z",
  "finished_at": null,
  "poll": "/v1/tools/runs/run_8c1f0e2a9b7d4f3e6a5c2b1d?wait=60"
}
```

Call `GET /v1/tools/runs/{run_id}` until `status` is no longer `running`. Pass `?wait=60` so each request waits on the server for up to 60 seconds instead of returning at once. You don't need to sleep between polls.

```bash title="cURL"
curl "https://lovelyrouter.com/v1/tools/runs/run_8c1f0e2a9b7d4f3e6a5c2b1d?wait=60" \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

The response has the same shape and status codes as the run request: `202` while running, `200` with `items` once finished. `results` stays `0` until the run finishes.

If you stop polling, the run still finishes and is settled automatically within minutes, so its hold is never stuck.

## Complete example

Start a run, poll until it finishes, then print the results. Set `LOVELY_API_KEY` first.

```bash title="cURL"
# Requires jq.
RUN=$(curl -s https://lovelyrouter.com/v1/tools/google-maps-places/run \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"input": {"searchTerms": ["coffee shop"], "location": "Austin, Texas, USA"}, "max_results": 20}')
ID=$(echo "$RUN" | jq -r .id)

while [ "$(echo "$RUN" | jq -r .status)" = "running" ]; do
  RUN=$(curl -s "https://lovelyrouter.com/v1/tools/runs/$ID?wait=60" \
    -H "Authorization: Bearer $LOVELY_API_KEY")
done

echo "$RUN" | jq '{status, results, charge_usd, error}'
echo "$RUN" | jq -r '.items[]? | "\(.title)  \(.rating)"'
```

```python title="Python"
import os
import requests

BASE = "https://lovelyrouter.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}

res = requests.post(
    f"{BASE}/tools/google-maps-places/run",
    headers=HEADERS,
    json={
        "input": {"searchTerms": ["coffee shop"], "location": "Austin, Texas, USA"},
        "max_results": 20,
    },
    timeout=90,
)
run = res.json()
if not res.ok:
    raise RuntimeError(run["error"]["message"])

# 202 = still running: poll, letting the server wait up to 60 s per request.
while res.status_code == 202:
    res = requests.get(f"{BASE}/tools/runs/{run['id']}", headers=HEADERS, params={"wait": 60}, timeout=90)
    res.raise_for_status()
    run = res.json()

print(f"{run['status']}: {run['results']} results, ${run['charge_usd']}")
for place in run["items"] or []:
    print(place["title"], place.get("rating"))
```

```js title="Node.js"
const BASE = 'https://lovelyrouter.com/v1';
const headers = {
  Authorization: `Bearer ${process.env.LOVELY_API_KEY}`,
  'content-type': 'application/json',
};

let res = await fetch(`${BASE}/tools/google-maps-places/run`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    input: { searchTerms: ['coffee shop'], location: 'Austin, Texas, USA' },
    max_results: 20,
  }),
});
let run = await res.json();
if (!res.ok) throw new Error(run.error.message);

// 202 = still running: poll, letting the server wait up to 60 s per request.
while (res.status === 202) {
  res = await fetch(`${BASE}/tools/runs/${run.id}?wait=60`, { headers });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  run = await res.json();
}

console.log(`${run.status}: ${run.results} results, $${run.charge_usd}`);
for (const place of run.items ?? []) console.log(place.title, place.rating);
```

## Page through results {#pagination}

By default a finished run returns all its results at once. For large runs, or to fetch them again, read them in pages with `offset` and `limit` on `GET /v1/tools/runs/{run_id}`:

```bash title="cURL"
curl "https://lovelyrouter.com/v1/tools/runs/run_8c1f0e2a9b7d4f3e6a5c2b1d?offset=100&limit=100" \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

- `offset` is the index of the first result to return. The default is `0`.
- `limit` is the page size, from 1 up to the run's `max_results`, which is also the default.
- The response echoes `offset` and sets `next_offset` to the offset of the next page, or `null` on the last page.

Reading results again is free. You are billed once, when the run settles.

If results can't be loaded for a moment, the response has `"items": null` and an `items_error` message. The run and its charge are unaffected. Request the same page again.

## Stop a run

Stop a run you no longer need with `POST /v1/tools/runs/{run_id}/stop`:

```bash title="cURL"
curl -X POST https://lovelyrouter.com/v1/tools/runs/run_8c1f0e2a9b7d4f3e6a5c2b1d/stop \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

The run ends with status `stopped`. You pay the per-result fee for results it delivered before it stopped, and no per-call fee. The response is the settled run with those results in `items`. Stopping a run that has already finished returns it unchanged.

Stopping usually takes a few seconds. If the run hasn't wound down within about 10 seconds, the response is `202` and you poll it as usual.

## The run object

Every run response is a run object. The fields you'll use most:

| Field | Meaning |
| --- | --- |
| `id` | Run id, `run_` followed by 24 characters. Use it to poll or stop the run. |
| `status` | `running`, `succeeded`, `failed`, `timed_out` or `stopped` |
| `results` | Results returned and billed. `0` until the run finishes. |
| `max_results` | The result cap that applies to this run |
| `charge_usd` | What the run cost. `null` until it is settled. |
| `reserved_usd` | The hold on your balance while the run is unsettled. `0` after. |
| `error` | Why the run ended early, or `null` |

[The run object](/docs/api/tools#the-run-object) in the API reference lists every field.

## Billing {#billing}

Starting a run holds its maximum cost from your balance: the per-call fee plus the per-result fee times `max_results`. While the run is going, that hold shows as `reserved_usd`. When the run ends, it is charged for what it returned, `charge_usd` is set, and the rest of the hold goes back to your balance.

| Outcome | Charge |
| --- | --- |
| `succeeded` | Per-call fee + per-result fee × `results` |
| `failed`, `timed_out` | Per-result fee × results delivered before it ended |
| `stopped` | Per-result fee × results delivered before you stopped it |
| Refused at start (any `4xx` or `5xx`) | Nothing. No hold is kept. |

The charge never exceeds the hold. Tool spend counts toward your balance and the key's spend limit like model spend, and every run is listed with its charge in [Tools](/app/tools) in your dashboard. For pricing in general, see [Data tools](/docs/tools#pricing).

### How max_results is set

The cap the run actually uses is in the run object's `max_results`, and the hold is based on it:

- **You set it.** Your value is used, lowered to the tool's `max_results` if it is higher.
- **You leave it out.** The tool's `default_results` applies. For tools that return one result per entry in an input list, such as a list of profile handles or URLs, the default is the length of that list.
- **Minimum run size.** A few tools only run in batches of a minimum size. If you ask for fewer, the cap is raised to that minimum. Those tools' `default_results` already reflects it.

`max_results` also caps the tool's own count field if it has one, such as `maxResults` or `maxReviewsPerCompany`, so you don't need to set both. If you set the count field lower, the run may return fewer results, but the hold is still based on `max_results`. An input list longer than `max_results` is cut to `max_results` entries.

### Insufficient balance

If your balance can't cover the hold, the run is refused with `402`:

```json
{
  "error": {
    "type": "insufficient_quota",
    "message": "Insufficient balance: this run can cost up to $0.04835 (200 places). Lower max_results or top up."
  }
}
```

Lower `max_results`, or [add credit](/docs/account/billing). If the hold would take the API key past its own spend limit, the run gets `402` with `This run can cost up to $…, which would take this API key past its spend limit.`

## Errors

Errors have the same shape as the rest of the API: an `error` object with a `type` and a readable `message`. Nothing is charged for a request that returns an error.

| Status | `type` | Cause | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_request_error` | The body isn't JSON, `input` isn't an object, a required input field is missing, or the tool rejected the input | Check `input` against the tool's `input_schema`. The message names missing fields. |
| `401` | `authentication_error` | Missing, invalid, revoked or expired API key | Send a valid key in `Authorization: Bearer`. |
| `402` | `insufficient_quota` | The balance or the key's spend limit can't cover the hold | Lower `max_results` or top up. |
| `404` | `not_found` | Unknown tool id, or a run id that doesn't exist or belongs to another account | Check the id. Find tools with `POST /v1/tools/discover`. |
| `413` | `invalid_request_error` | `input` is over 50,000 characters | Send less input per run. |
| `429` | `rate_limit_exceeded` | 10 runs are already in progress on your account | Wait for one to finish, then retry. |
| `502` | `tool_unavailable` | The tool could not start | Retry shortly. |

See [Errors](/docs/api/errors) for how errors work across the API.

## List your runs

`GET /v1/tools/runs` returns your account's runs, newest first, without their items:

```bash title="cURL"
curl "https://lovelyrouter.com/v1/tools/runs?limit=20" \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

`limit` is 1 to 100 (default 20). When there are more runs, the response's `next` is a cursor: pass it as `?before=` to get the next page. When `next` is `null`, you have them all.

## Run the browser agent

The browser agent is in the catalog as `browser-agent`, so `POST /v1/tools/browser-agent/run` starts it too. Put its `goal` and `start_url` in `input`, and set `max_results` to the maximum number of steps. It returns `202` and a browser run, which you follow with the browser API. See [Browser agent](/docs/browser-agent).
