# Watches

> Run saved AI-search queries on a schedule for continuous visibility monitoring without maintaining your own cron job.

A **Watch** re-runs a saved `(query, surfaces, regions, extract)` on a cadence. Add a `webhookUrl` to receive each dispatched run exactly like any other async search.

Use watches for continuous GEO and rank monitoring without operating a scheduler on your side.

Create watches three equivalent ways — same validation, plan limits, and billing on every path:

- **REST** — [`POST /v1/watches`](/api-reference/watches/create) and the rest of the [watch endpoints](/api-reference/watches/list).
- **MCP** — the `create_watch` / `list_watches` / `update_watch` / `delete_watch` tools on the [hosted MCP server](/mcp), built for agents.
- **Dashboard** — the Watches page in your [workspace](https://aisearchapi.dev/home).

<Info>
  Creating a watch is free. Each **dispatched run** bills per surface × region
  exactly like a normal async search, with the same success-only refunds for
  empty or failed captures. A skipped run costs nothing. See
  [Credits](/guides/credits).
</Info>

## Schedules

Use a named token for common cadences:

```json
{ "schedule": { "every": "1d" } }
```

Allowed tokens are `15m`, `30m`, `1h`, `6h`, `12h`, `1d`, and `7d`. For an off-grid cadence, send a positive integer:

```json
{ "schedule": { "intervalMinutes": 90 } }
```

The public Watch always echoes both fields:

```json
{ "schedule": { "every": "1h", "intervalMinutes": 90 } }
```

For an off-grid interval, `every` is a display convenience using the nearest token. `intervalMinutes` is the source of truth.

## Plan limits

Each plan sets an active-watch ceiling and a minimum interval.

| Plan         | Max active watches | Minimum interval |
| ------------ | ------------------ | ---------------- |
| `free`       | 2                  | 1440 min (daily) |
| `starter`    | 10                 | 60 min (hourly)  |
| `growth`     | 25                 | 30 min           |
| `scale`      | 100                | 15 min           |
| `enterprise` | 500                | 15 min           |

An interval below the plan floor returns `422 WATCH_INTERVAL_TOO_SHORT`; the message names the floor and plan. Creating past the active-watch ceiling returns `403 WATCH_LIMIT_EXCEEDED`; the message names the ceiling and plan.

## Lifecycle

A watch is always in one of three states:

| Status     | Meaning                                                |
| ---------- | ------------------------------------------------------ |
| `active`   | Scheduled ticks can dispatch new search jobs.          |
| `paused`   | Scheduling is suspended until the watch is resumed.    |
| `canceled` | Terminal soft-delete state; no new runs are scheduled. |

Pause or resume with [`PATCH /v1/watches/:id`](/api-reference/watches/update). A manual pause sets `pausedReason: "user"`. Resuming a paused watch clears `pausedReason` and sets `nextRunAt` to `now + intervalMinutes`.

Use [`DELETE /v1/watches/:id`](/api-reference/watches/delete) to cancel. Cancellation preserves the watch row and run history.

### Automatic pause

If three scheduled runs in a row are skipped for insufficient credits, the watch becomes `paused` with `pausedReason: "insufficient_credits"`. A successful dispatch resets the consecutive-failure counter to zero.

<Note>
  Auto-pause prevents an underfunded watch from producing an unbounded stream of
  skipped ticks. After restoring credits, resume it explicitly; the next run is
  scheduled from the resume time.
</Note>

## Run history

Every scheduled tick that reaches a watch creates one run-history row. Read the ten newest runs with [`GET /v1/watches/:id`](/api-reference/watches/get), or page through the full history with [`GET /v1/watches/:id/runs`](/api-reference/watches/runs).

| Status            | Meaning                                                                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `dispatched`      | A real search job was created and `jobId` is set. Poll it or consume its webhook delivery.                                    |
| `skipped_credits` | Balance was insufficient at dispatch time; nothing was billed. Three consecutive occurrences auto-pause the watch.            |
| `skipped_limit`   | An account or plan ceiling prevented dispatch; nothing was billed.                                                            |
| `failed`          | The scheduled attempt itself failed to submit. A normal captured-but-empty or failed job remains `dispatched` with a `jobId`. |

For `dispatched`, read `jobId` with [`GET /v1/jobs/:id`](/api-reference/get-job) exactly like any other async parent job. If the watch has a `webhookUrl`, each child delivery uses the same signature, retry, and dedupe semantics described in [Webhooks](/guides/webhooks).

## Create and inspect a watch

Create a daily watch, then read it to see `lastRunAt`, `lastJobId`, and the newest run records as ticks dispatch.

<CodeGroup>

```bash cURL
WATCH_ID=$(curl -s -X POST https://api.aisearchapi.dev/v1/watches \
  -H "Authorization: Bearer $AISEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "best crm for startups",
    "surfaces": ["chatgpt", "perplexity"],
    "regions": [{ "country": "US" }],
    "extract": { "targets": ["HubSpot", "attio.com"] },
    "schedule": { "every": "1d" }
  }' | jq -r '.watch.id')

curl "https://api.aisearchapi.dev/v1/watches/$WATCH_ID" \
  -H "Authorization: Bearer $AISEARCH_API_KEY"
```

```javascript Node
const created = await fetch('https://api.aisearchapi.dev/v1/watches', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.AISEARCH_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'best crm for startups',
    surfaces: ['chatgpt', 'perplexity'],
    regions: [{ country: 'US' }],
    extract: { targets: ['HubSpot', 'attio.com'] },
    schedule: { every: '1d' },
  }),
});
const { watch } = await created.json();

const inspected = await fetch(
  `https://api.aisearchapi.dev/v1/watches/${watch.id}`,
  { headers: { Authorization: `Bearer ${process.env.AISEARCH_API_KEY}` } },
);
console.log(await inspected.json());
```

```python Python
import os, requests

headers = {"Authorization": f"Bearer {os.environ['AISEARCH_API_KEY']}"}
created = requests.post(
    "https://api.aisearchapi.dev/v1/watches",
    headers={**headers, "Content-Type": "application/json"},
    json={
        "query": "best crm for startups",
        "surfaces": ["chatgpt", "perplexity"],
        "regions": [{"country": "US"}],
        "extract": {"targets": ["HubSpot", "attio.com"]},
        "schedule": {"every": "1d"},
    },
)
watch_id = created.json()["watch"]["id"]

inspected = requests.get(
    f"https://api.aisearchapi.dev/v1/watches/{watch_id}",
    headers=headers,
)
print(inspected.json())
```

</CodeGroup>

## Related

<CardGroup cols={2}>
  <Card title="Create a watch" icon="plus" href="/api-reference/watches/create">
    Request fields, schedules, and creation errors.
  </Card>
  <Card title="Get a watch" icon="file-lines" href="/api-reference/watches/get">
    Read configuration and the ten newest runs.
  </Card>
  <Card
    title="Update a watch"
    icon="pen-to-square"
    href="/api-reference/watches/update"
  >
    Pause, resume, or change the cadence and delivery.
  </Card>
  <Card
    title="Watch runs"
    icon="clock-rotate-left"
    href="/api-reference/watches/runs"
  >
    Page through complete run history.
  </Card>
  <Card title="Webhooks" icon="webhook" href="/guides/webhooks">
    Verify the signed deliveries from dispatched runs.
  </Card>
  <Card title="Credits" icon="coins" href="/guides/credits">
    Per-surface, success-only billing for each dispatch.
  </Card>
</CardGroup>
