# Create a watch

> Save a query and schedule recurring AI-surface captures without running your own cron job.

`POST /v1/watches` creates an active watch that re-runs a saved query on a cadence.

```http
POST https://api.aisearchapi.dev/v1/watches
```

Every request needs `Authorization: Bearer <API_KEY>` and `Content-Type: application/json`.

<Info>
  Creating a watch is **free**. Credits are reserved only when a scheduled run
  dispatches, using the same per-surface, success-only billing as an async
  search. A skipped run costs nothing.
</Info>

<Note>
  Watches can also be created from the dashboard Watches page or by an agent
  through the `create_watch` tool on the [MCP server](/mcp) — every path hits
  this same endpoint with identical validation, plan limits, and billing.
</Note>

## Tenant scope

Restricted (`sk_*`) keys always create watches for their own owner. Internal keys can act on behalf of one tenant for reads **and writes** by sending `?owner=<ownerId>` or `X-AISearch-On-Behalf-Of: <ownerId>`; the query parameter wins when both are present. A restricted key's override is ignored.

When an override is applied, the response adds a top-level `owner` field and an `X-AISearch-Owner` header containing the enforced owner id.

<ParamField query="owner" type="string">
  Internal keys only. Owner id to enforce for this write. Overrides the
  `X-AISearch-On-Behalf-Of` header when both are sent.
</ParamField>

## Body

<ParamField body="name" type="string" default="">
  Optional display name. Omitted watches use an empty string.
</ParamField>

<ParamField body="query" type="string" required>
  The exact prompt to run on every tick. Non-empty, up to **2000 characters**.
  This endpoint does not accept the `prompt` alias.
</ParamField>

<ParamField body="surfaces" type="string[]" required>
  One or more surfaces. Allowed values: `chatgpt`, `claude`, `perplexity`,
  `gemini`, `copilot`, `google_ai_overview`, `google_ai_mode`, `google_search`,
  `google_news`.
</ParamField>

<ParamField body="regions" type="object[]">
  Up to **10** regions. Omitted: one untargeted (`GLOBAL`) run per surface.

  <Expandable title="region object">
    <ParamField body="country" type="string" required>
      ISO-3166 alpha-2 country code, e.g. `US`.
    </ParamField>
    <ParamField body="state" type="string">
      Optional state or first-level administrative area.
    </ParamField>
    <ParamField body="city" type="string">
      Optional city.
    </ParamField>
    <ParamField body="language" type="string">
      Optional BCP-47 language hint.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="extract" type="boolean | object">
  Auto Extract configuration re-applied on every dispatched run. `true` and
  `{}` extract opportunistically; `false` or omission disables extraction.

  <Expandable title="extract object">
    <ParamField body="targets" type="string[]">
      Up to 10 pinned brand names or bare domains, each non-empty and at most
      100 characters. Pinned targets are reported whether present or absent.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="webhookUrl" type="string">
  Public HTTPS endpoint that receives each dispatched run's signed deliveries.
  Private, loopback, link-local, and metadata destinations are rejected.
</ParamField>

<ParamField body="schedule" type="object" required>
  Cadence in one of two forms. The interval must meet your plan's floor.

  <Expandable title="schedule object">
    <ParamField body="every" type="string">
      One of `15m`, `30m`, `1h`, `6h`, `12h`, `1d`, or `7d`.
    </ParamField>
    <ParamField body="intervalMinutes" type="integer">
      Positive integer for an off-grid cadence. Send either this field or
      `every`, not both.
    </ParamField>
  </Expandable>
</ParamField>

## Request

<CodeGroup>

```bash cURL
curl -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" }],
    "schedule": { "every": "1d" }
  }'
```

```javascript Node
const res = 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' }],
    schedule: { every: '1d' },
  }),
});

const { watch } = await res.json();
```

```python Python
import os, requests

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

watch = res.json()["watch"]
```

</CodeGroup>

## Response

```json 201 Created
{
  "watch": {
    "id": "watch_8f2c1a7e",
    "name": "",
    "query": "best crm for startups",
    "surfaces": ["chatgpt", "perplexity"],
    "regions": [{ "country": "US" }],
    "extract": null,
    "webhookUrl": null,
    "schedule": { "every": "1d", "intervalMinutes": 1440 },
    "status": "active",
    "pausedReason": null,
    "nextRunAt": "2026-07-12T14:00:00Z",
    "lastRunAt": null,
    "lastJobId": null,
    "createdAt": "2026-07-11T14:00:00Z"
  }
}
```

<ResponseField name="watch" type="object">
  The created public Watch.

  <Expandable title="watch">
    <ResponseField name="id" type="string">Watch id (`watch_...`).</ResponseField>
    <ResponseField name="name" type="string">Display name; `""` when omitted.</ResponseField>
    <ResponseField name="query" type="string">Saved query.</ResponseField>
    <ResponseField name="surfaces" type="string[]">Surfaces run on each tick.</ResponseField>
    <ResponseField name="regions" type="object[]">Saved region objects.</ResponseField>
    <ResponseField name="extract" type="object | null">Normalized extraction config, or `null` when disabled.</ResponseField>
    <ResponseField name="webhookUrl" type="string | null">Delivery destination, or `null`.</ResponseField>
    <ResponseField name="schedule" type="object">Both `every` and the source-of-truth `intervalMinutes`.</ResponseField>
    <ResponseField name="status" type="string">`active`, `paused`, or `canceled`.</ResponseField>
    <ResponseField name="pausedReason" type="string | null">Pause reason when present, e.g. `user` or `insufficient_credits`.</ResponseField>
    <ResponseField name="nextRunAt" type="string">Next scheduled tick, ISO-8601.</ResponseField>
    <ResponseField name="lastRunAt" type="string | null">Most recent tick, or `null`.</ResponseField>
    <ResponseField name="lastJobId" type="string | null">Parent job id from the most recent run, when present.</ResponseField>
    <ResponseField name="createdAt" type="string">Creation time, ISO-8601.</ResponseField>
  </Expandable>
</ResponseField>

<Note>
  Responses always echo both schedule fields. For an off-grid interval, `every`
  is a display convenience; `intervalMinutes` is the source of truth.
</Note>

## Errors

| Code                       | Status | When                                                                  |
| -------------------------- | ------ | --------------------------------------------------------------------- |
| `VALIDATION_FAILED`        | 400    | A body field, schedule form, region, or extraction target is invalid. |
| `AUTH_INVALID`             | 401    | The API key is missing, invalid, or revoked.                          |
| `WATCH_LIMIT_EXCEEDED`     | 403    | Creating the watch would exceed the plan's active-watch ceiling.      |
| `WATCH_INTERVAL_TOO_SHORT` | 422    | The requested interval is below the plan's minimum.                   |

Creating a watch never returns `INSUFFICIENT_CREDITS`: creation costs 0 credits, and credits are considered only when a run dispatches.

```json 422
{
  "code": "WATCH_INTERVAL_TOO_SHORT",
  "error": "minimum watch interval is 1440 minutes on the free plan",
  "request_id": "req_9f2c1a7e4b5d48f1a3c6e8d0b2a4f6c8",
  "docs_url": "https://docs.aisearchapi.dev/guides/errors#watch_interval_too_short"
}
```

## Related

<CardGroup cols={2}>
  <Card title="Watches" icon="satellite-dish" href="/guides/watches">
    Scheduling, limits, lifecycle, billing, and run history.
  </Card>
  <Card title="List watches" icon="list-ul" href="/api-reference/watches/list">
    Page through the owner's watches.
  </Card>
  <Card
    title="Update a watch"
    icon="pen-to-square"
    href="/api-reference/watches/update"
  >
    Change the cadence, extraction, webhook, or status.
  </Card>
  <Card title="Webhooks" icon="webhook" href="/guides/webhooks">
    Verify signed deliveries from dispatched runs.
  </Card>
</CardGroup>
