# URL Watch endpoints

> List, create, update, and delete URL Watch subscriptions and read their crawl diffs over HTTP, authenticated with an organization management key (fhm_).

These endpoints create and manage [URL Watch](/url-watch/overview) subscriptions and read their
diffs. Like the [tap endpoints](/api-reference/management-key-endpoints), they require a
[management key](/organizations/management-keys) (`fhm_`). Base URL: `https://api.firehose.com`.

A management key acts as the user who created it. These endpoints only see and modify **that
user's own** watches within the organization — a watch owned by another member returns `404`.

<Callout type="info">
  URL Watch must be available on your [plan](/billing/plans). It's unavailable on the **API only**
  plan, where any create call is rejected with `422`.
</Callout>

## List watches

```text
GET /v1/url-watch?limit=50
```

`limit` is optional, from `1` to your plan's **maximum watched URLs** (the default). Returns your
watches, newest first.

```json
{
  "data": [
    {
      "id": "uuid",
      "url": "https://example.com/pricing",
      "frequency_minutes": 360,
      "status": "active",
      "change_count": 4,
      "last_checked_at": "2026-06-08T09:00:00+00:00",
      "created_at": "2026-06-01T00:00:00+00:00"
    }
  ]
}
```

| Field | Description |
| --- | --- |
| `id` | Subscription identifier, used in the path of the other endpoints. |
| `url` | The watched URL. |
| `frequency_minutes` | Crawl cadence in minutes (see [Frequency values](#frequency-values)). |
| `status` | `active` or `paused`. |
| `change_count` | Number of crawls so far that recorded a change. |
| `last_checked_at` | Timestamp of the most recent crawl, or `null` if it hasn't been crawled yet. |
| `created_at` | When the watch was created. |

## Create watch

```text
POST /v1/url-watch
Content-Type: application/json

{ "url": "https://example.com/pricing", "frequency_minutes": 360 }
```

Both fields are required. `url` must be a valid URL (max 2048 characters) and `frequency_minutes`
must be one of the [allowed values](#frequency-values) for your plan. Returns `201` with the new
subscription; the first crawl is queued immediately.

Common `422` responses:

- **Invalid frequency** — not an allowed value, or faster than your plan permits.
- **Watched-URL limit reached** — already at your plan's maximum watched URLs.
- **Monthly checks exhausted** — this month's [check quota](/url-watch/quotas) is used up.
- **Duplicate URL** — `You are already watching this URL.`, or
  `This URL is already being watched in your organization.` if a teammate watches it.

## Get watch (with diffs)

```text
GET /v1/url-watch/:id?limit=50
```

Returns the subscription plus its crawl history, newest first. `limit` is optional, from `1` to
`100` (default `50`).

```json
{
  "data": {
    "id": "uuid",
    "url": "https://example.com/pricing",
    "frequency_minutes": 360,
    "status": "active",
    "change_count": 4,
    "last_checked_at": "2026-06-08T09:00:00+00:00",
    "created_at": "2026-06-01T00:00:00+00:00"
  },
  "diffs": [
    {
      "diff": { "hunks": [[{ "del": ["$19/mo"], "ins": ["$24/mo"] }]] },
      "has_changes": true,
      "created_at": "2026-06-08T09:00:00+00:00"
    },
    {
      "diff": null,
      "has_changes": false,
      "created_at": "2026-06-08T03:00:00+00:00"
    }
  ]
}
```

Each entry is one crawl. When the page changed, `has_changes` is `true` and `diff` holds the
change (see [Diff shape](#diff-shape)). A crawl that found no change — or that **errored** — has
`has_changes: false` and `diff: null`; the error detail itself isn't returned over the API.

## Update watch

```text
PUT /v1/url-watch/:id
Content-Type: application/json

{ "frequency_minutes": 720, "status": "paused" }
```

Both fields are optional. `frequency_minutes` must be an [allowed value](#frequency-values);
`status` is `active` or `paused`. Resuming a paused watch (`status: "active"`) while your monthly
checks are exhausted returns `422`. Returns the updated subscription.

## Delete watch

```text
DELETE /v1/url-watch/:id
```

Returns `204` with no content.

## Frequency values

`frequency_minutes` accepts these values, subject to your plan's fastest interval — slower cadences
are always allowed:

| Minutes | Cadence | | Minutes | Cadence |
| --- | --- | --- | --- | --- |
| `5` | Every 5 minutes | | `720` | Every 12 hours |
| `10` | Every 10 minutes | | `1440` | Daily |
| `30` | Every 30 minutes | | `2880` | Every 2 days |
| `60` | Hourly | | `7200` | Every 5 days |
| `180` | Every 3 hours | | `10080` | Weekly |
| `360` | Every 6 hours | | | |

The fastest interval allowed is 3 hours on Free, 10 minutes on Starter, and 5 minutes on Advanced
and Business. See [Quotas & limits](/url-watch/quotas).

## Diff shape

`diff` is the stored diff, decoded as JSON. The current format groups the change into `hunks` — a
list of hunks, each a list of blocks with optional `ctx` (unchanged context), `del` (removed), and
`ins` (added) line arrays:

```json
{
  "hunks": [
    [
      { "ctx": ["Pricing"] },
      { "del": ["$19/mo"], "ins": ["$24/mo"] }
    ]
  ]
}
```

Older entries may use a flat `chunks` array instead — `{ "chunks": [{ "typ": "ins", "text": "..." }] }`,
where `typ` is `ins` or `del`. This is the same diff rendered in the dashboard; see
[Diffs & crawl history](/url-watch/diffs-and-history).

<Callout type="info">
  See [Errors & limits](/api-reference/errors-and-limits) for status codes shared across the API.
</Callout>

## Next steps

<CardGrid>
  <Card title="Quotas & limits" href="/url-watch/quotas">
    How checks are counted and the caps that drive the `422` responses above.
  </Card>
  <Card title="Creating watches" href="/url-watch/creating-watches">
    The same workflow from the dashboard.
  </Card>
  <Card title="Management keys" href="/organizations/management-keys">
    Create and rotate the `fhm_` key these endpoints use.
  </Card>
</CardGrid>
