---
title: "Watch a company or a saved search"
method: POST
path: "/v1/watches"
---

# Watch a company or a saved search

`POST /v1/watches`

## Request

- Base URL: `https://api.reqbeat.com`
- URL: `https://api.reqbeat.com/v1/watches`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Subscribe to hiring events on a registered webhook -- scoped to the
caller's own customer: `webhook_endpoint_id` must belong to the same
customer. Meters one `watch` unit.

The watch covers one company (`company_id`) or a saved search
(`criteria`) -- exactly one, and a criteria outside the closed
vocabulary is a 422 raised before any row is written. So is an
`event_types` that is empty or names anything but `opened`,
`reobserved`, `reposted` or `closed`: such a watch could never fire,
so it is refused rather than created and metered.

A free key holds a limited number of watches AT ONCE; the one past that
is a 402 carrying the upgrade path, checked before the watch is created,
so a walled call leaves no subscription behind. Cancelling a watch
returns the slot.

Requires the `webhooks` scope.

## Headers

- `Idempotency-Key` string, nullable

## Request body

- CreateWatchRequest: Scope the watch with exactly one of `company_id` (one company) or `criteria` (a saved search). `event_types` names which hiring events fire it, at least one of: `opened` (a req seen for the first time), `reobserved` (a known req seen again on a later day), `reposted` (a req that came back after it was gone) and `closed` (a req verified gone from its source). Any other value is a 422 and creates nothing.
  - `company_id` integer, nullable
  - `criteria` Criteria: A closed AND-of-ORs over three namespaced groups. At least one field must be constrained.
    - `event` CriteriaEvent: The `event` criteria group: AND across fields, OR within a field.
      - `company_id` integer[]
      - `event_type` string[]
      - `board` string[]
      - `source_type` string[]
      - `ats_vendor` string[]
      - `function` string[]
      - `country` string[]
    - `req` CriteriaReq: The `req` criteria group: AND across fields, OR within a field.
      - `seniority_level` string[]
      - `function` string[]
      - `remote_type` string[]
      - `skills` string[]
      - `q` string[]
      - `location` string[]
      - `q_excludes` string[]
    - `company` CriteriaCompany: The `company` criteria group: AND across fields, OR within a field.
      - `industry` string[]
      - `company_kind` string[]
      - `country` string[]
      - `headcount_band` string[]
      - `industry_terms` string[]
    - `max_fires_per_hour` integer
    - `dedup_window_days` integer
  - `event_types` string[], required
  - `webhook_endpoint_id` integer, required

## Response `201`

Successful Response

- WatchResponse: One live watch, as its owner sees it (signalsapi-3963). `criteria` is echoed exactly as it was authored -- `watch_criteria.parse()` normalizes on the read side at fan-out, so the row is the customer's own text. The three rate fields (signalsapi-4442) are what tell a CAPPED watch from a quiet one: without them a watch saturating its declared `max_fires_per_hour` -- whose excess fires are dropped, not queued -- looked byte-identical to one nobody is hiring against. The four heartbeat fields (signalsapi-4443) answer the question one rung before that: a watch firing NOTHING was indistinguishable from one that is broken, wall-blocked, or matching a criteria set that can never match. `last_fired_at` is when it last matched anything (`null` if never) and `status` is one of `watch_service.WATCH_STATUSES` -- the one-word reason, never a boolean, because "upgrade the plan", "raise your own cap" and "fix your endpoint" are three different fixes that must not render the same. `POST /v1/watches` returns this same shape (signalsapi-5505) rather than a bare `{id}` -- a fresh watch's health fields are well-defined (zero fires, not yet rate-limited) and a caller that wants to show the new watch immediately would otherwise have to round-trip `GET /v1/watches` for a response its own create call already has the data for.
  - `id` integer, required
  - `company_id` integer, nullable
  - `criteria` Criteria: A closed AND-of-ORs over three namespaced groups. At least one field must be constrained.
    - `event` CriteriaEvent: The `event` criteria group: AND across fields, OR within a field.
      - `company_id` integer[]
      - `event_type` string[]
      - `board` string[]
      - `source_type` string[]
      - `ats_vendor` string[]
      - `function` string[]
      - `country` string[]
    - `req` CriteriaReq: The `req` criteria group: AND across fields, OR within a field.
      - `seniority_level` string[]
      - `function` string[]
      - `remote_type` string[]
      - `skills` string[]
      - `q` string[]
      - `location` string[]
      - `q_excludes` string[]
    - `company` CriteriaCompany: The `company` criteria group: AND across fields, OR within a field.
      - `industry` string[]
      - `company_kind` string[]
      - `country` string[]
      - `headcount_band` string[]
      - `industry_terms` string[]
    - `max_fires_per_hour` integer
    - `dedup_window_days` integer
  - `event_types` string[], required
  - `webhook_endpoint_id` integer, required
  - `created_at` string, date-time, required
  - `fires_last_hour` integer, required
  - `max_fires_per_hour` integer, required
  - `rate_limited` boolean, required
  - `last_fired_at` string, date-time, nullable
  - `fires_last_7d` integer, required
  - `fires_last_30d` integer, required
  - `status` string, required

## Other responses

- `401`: No API key was sent.
- `402`: The key is over its plan's rolling call quota, or the account is past its spend cap. `upgrade_url` and `purchase_url` are the ways out.
- `403`: The presented key is invalid or revoked, or (see `x-required-scope` on this operation) it does not carry the scope this operation requires.
- `422`: Validation Error
- `429`: Too many calls in the current 60-second window. The limit is per API key, set by the key's plan, and shared by every metered REST route and MCP tool; the shared demo key is limited per IP address instead. Wait `Retry-After` seconds and retry. The `RateLimit-*` headers are absent when `reason` is `key_throttled`.

---

[API](https://skmtc.dev/reqbeat/apis/reqbeat-api.md) · [All operations](https://skmtc.dev/reqbeat/apis/reqbeat-api/llms.txt) · [OpenAPI document](https://skmtc.dev/reqbeat/apis/reqbeat-api/revisions/4758e5957e8c?raw)
