> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crustdata.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Person Entity Watcher

> Watch a list of people you supply and get notified the moment their profile changes — a new job, a title move, a fresh certification. You supply the list; the watcher delivers the diff.

A **Person Entity Watcher** monitors **a list of people you supply** (by profile URL) and notifies you when *their* profiles change. You describe what to watch for; each scheduled run compares every person against their last snapshot and delivers only what changed. The list is **yours to edit any time** — [add or remove people](#edit-the-watched-list-any-time) without recreating the watch.

Where [Person Discovery Watchers](/watcher-docs/person/discovery) **find new** people matching a filter, Entity Watchers **track a known set** — an ATS of candidates, key contacts, a target roster — and surface *movement* within it:

* A tracked person **starts a new job** or **changes their title or headline** — a strong "in motion" signal.
* A tracked person **earns a certification** or **receives an award**.
* A tracked person **relocates**, **adds a degree**, or **lists a new skill**.

Create a person watch with:

```
POST https://api.crustdata.com/watch/person
```

<Note>
  Every request requires the `x-api-version: 2025-11-01` header and a Bearer
  token. Replace `YOUR_API_KEY` in each example with your API key. This
  endpoint is **open to all API customers** — no per-endpoint grant is needed.
</Note>

<Callout icon="coins" color="#5345e4">
  <strong>Pricing — you pay only for notifications.</strong> The first run of every
  watch is a <strong>free baseline</strong> that records each person's starting
  values internally (for diffing only — it isn't delivered) and never fires. After
  that you're charged <strong>5 credits per notification</strong> — 5 credits for
  each watched person that changed on a run. A run that surfaces nothing costs
  nothing, and the price is the same whether you check hourly or monthly. If your
  balance is too low, the run is suspended rather than partially delivered. See
  [Pricing](/general/pricing) for the canonical rates.
</Callout>

## How a person entity watcher runs

<Steps>
  <Step title="Create the watch">
    `POST` your `entities` (people to watch), a `track` (what to watch for), a
    `config` (schedule + caps), and one or more `notifications` channels. The response
    returns the full watch object, including its `id`.
  </Step>

  <Step title="Baseline run (free)">
    The first run records an internal **snapshot** of the fields you're tracking for
    each person — just enough to diff against next time. With no prior snapshot to
    compare, the baseline **never fires and is never charged**, and isn't itself
    delivered — it only establishes "before". To read a person's current profile on
    demand, use the Enrich APIs.
  </Step>

  <Step title="Recurring runs">
    On your schedule (`every_hours`), the watcher re-checks each person, diffs them
    against their last snapshot, and delivers a notification for every person whose
    `track` condition just became true. You're charged 5 credits per notification.
  </Step>
</Steps>

<Note>
  An Entity Watcher only fires on a **transition** — the moment a value crosses
  from its previous state to a new one. It never fires on the baseline, and won't
  re-notify about a change it already reported. To read a list's current state on
  demand instead of watching for change, use the
  [Person Enrich](/person-docs/enrichment/introduction) API.
</Note>

## What you can track

The `track` describes the change that triggers a notification — a tree of condition
**leaves**, optionally combined with `and` / `or` groups.

A leaf has the shape:

```json theme={"theme":"vitesse-black"}
{ "field": "<dot-path into the profile>", "type": "<operator>", "value": "<optional>" }
```

| Operator                                | Applies to                                              | Fires when                                                                               |
| --------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `changed`                               | a **scalar** field (e.g. `basic_profile.current_title`) | the value becomes different from the last snapshot                                       |
| `added`                                 | an **array** field (e.g. `certifications`, `honors`)    | a new element appears in the array                                                       |
| `>`, `<`, `=`, `!=`, `=>` (≥), `=<` (≤) | a scalar, or a predicate on an array element            | the comparison flips from false to true (e.g. `professional_network.connections => 500`) |

<Note>
  Crustdata writes "greater-than-or-equal" as **`=>`** and "less-than-or-equal" as
  **`=<`** — not `>=` / `<=`. Those reversed forms return `unknown operator`.
</Note>

`value` is required for the comparison operators and omitted for `changed` and `added`.

Combine leaves with a group node — `{ "op": "and" | "or", "conditions": [ … ] }` — to
watch several signals at once. An `or` group fires if **any** child fires; an `and`
group fires only when **all** conditions hold and at least one just became true.

### Common person signals

Examples only — a person profile has **200+ addressable fields**, and you can track any of them (any scalar with `changed`, any array with `added`).

| Signal                   | `field`                                 | `type`    |
| ------------------------ | --------------------------------------- | --------- |
| Started a new job        | `experience.employment_details.current` | `added`   |
| Title changed            | `basic_profile.current_title`           | `changed` |
| Headline changed         | `basic_profile.headline`                | `changed` |
| Relocated                | `basic_profile.location.raw`            | `changed` |
| New certification        | `certifications`                        | `added`   |
| New award or honor       | `honors`                                | `added`   |
| New education / degree   | `education.schools`                     | `added`   |
| New skill listed         | `skills.professional_network_skills`    | `added`   |
| Connection count changed | `professional_network.connections`      | `changed` |

<Note>
  `field` paths address the same profile structure the Enrich API returns, so browse
  the full set of trackable fields in the
  [Person Enrich](/person-docs/enrichment/reference) reference. `changed` requires a
  **scalar** path and `added` an **array** path — mixing them returns a `400` (for
  example, `"changed"` on the array `experience.employment_details.current` is
  rejected; watch `basic_profile.current_title` instead).
</Note>

### Recipes: combine signals

Group leaves with `or` to fire on **any** of several signals, or `and` to require them together. These `track` blocks are all live-verified.

<CodeGroup>
  ```json Any career move theme={"theme":"vitesse-black"}
  {
    "op": "or",
    "conditions": [
      { "field": "experience.employment_details.current", "type": "added" },
      { "field": "basic_profile.current_title", "type": "changed" }
    ]
  }
  ```

  ```json New credential theme={"theme":"vitesse-black"}
  {
    "op": "or",
    "conditions": [
      { "field": "certifications", "type": "added" },
      { "field": "honors", "type": "added" },
      { "field": "education.schools", "type": "added" }
    ]
  }
  ```
</CodeGroup>

Drop either into the `track` field of a create request. Groups nest, so you can mix `and`/`or` — e.g. *"a title change **and** a new certification."*

## `track` and `fields` are different

Two independent knobs, and the distinction matters:

| Key      | Controls                           | Answers                                                  |
| -------- | ---------------------------------- | -------------------------------------------------------- |
| `track`  | **When** the watch fires           | *"What change do I want to be notified about?"*          |
| `fields` | **What** the notification delivers | *"What data about the person do I want in the payload?"* |

Set them independently. Track one field but deliver many (watch for a title change,
yet receive the full employment history and education), or track many and deliver few.
`track` decides *whether* you get a notification; `fields` decides *what's inside* it.

<Warning>
  **`fields` defaults to a minimal projection.** If you omit `fields`, the delivered
  `record` contains only `basic_profile` and `social_handles`.

  The field you're *tracking* is **not** automatically added to the payload — the
  fired `changes` array always tells you exactly what moved, but to get the
  surrounding profile data in `record`, request it in `fields`.
</Warning>

### Requesting all fields

`fields` does **not** affect pricing — an entity watch is a flat **5 credits per
notification** regardless of how many field groups you deliver (see
[Pricing](/general/pricing)). There's no cost reason to keep the payload thin;
request whatever your workflow needs.

There's no wildcard — to receive the complete record, list every field group your
API key is entitled to. `fields` is a top-level key (a sibling of `track`), fixed at
create time.

```json Person — all fields theme={"theme":"vitesse-black"}
{
  "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] },
  "track": { "field": "experience.employment_details.current", "type": "added" },
  "fields": [
    "basic_profile",
    "professional_network",
    "social_handles",
    "experience",
    "education",
    "skills",
    "certifications",
    "honors",
    "contact",
    "dev_platform_profiles"
  ]
}
```

<Note>
  Some groups require a field-level entitlement on your key (e.g. `certifications`).
  Requesting a group you aren't entitled to returns a field-permission error, so list
  only the groups your key can access. The group names match the Enrich API — see the
  [Person Enrich](/person-docs/enrichment/reference) `fields` reference.
</Note>

## Choosing the entities

`entities` maps an **identifier type** to a list of values. A single watch holds up
to **10,000 people**.

| Accepted identifier keys            |
| ----------------------------------- |
| `professional_network_profile_urls` |

```json theme={"theme":"vitesse-black"}
{ "entities": { "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"] } }
```

## Schedule and limits

The `config` block controls timing and result caps:

| Field                 | Required | Meaning                                                                                                                                                                |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trigger.type`        | yes      | Must be `"interval"`.                                                                                                                                                  |
| `trigger.every_hours` | yes      | How often the watch runs, in hours (integer ≥ 1). Because you pay per notification, a higher frequency only makes changes surface sooner — it doesn't raise the price. |
| `max_results_per_run` | no       | Cap on notifications per run (1–1000, default 25).                                                                                                                     |
| `preferred_hour`      | no       | Hour of day (0–23) to prefer for the run.                                                                                                                              |
| `expires_at`          | no       | Auto-expire date, `YYYY-MM-DD`.                                                                                                                                        |

## Delivery channels

Add one or more channels to `notifications` and every change fans out to all of them.

| Channel     | Shape                                                                      |
| ----------- | -------------------------------------------------------------------------- |
| Webhook     | `{ "type": "webhook", "url": "https://…", "headers": { … } }`              |
| Slack       | `{ "type": "slack", "webhook_url": "https://hooks.slack.com/services/…" }` |
| Google Chat | `{ "type": "google_chat", "url": "https://chat.googleapis.com/…" }`        |

<Note>
  At least one notification channel is required today. Every delivery is also
  recorded, so beyond the live push you can re-read a watch's past runs from the
  [run-history endpoint](#pulled-from-run-history).
</Note>

## Quick start

Watch one person and get a webhook whenever they start a new job. This request and
response are real.

<CodeGroup>
  ```bash Request theme={"theme":"vitesse-black"}
  curl --request POST \
    --url https://api.crustdata.com/watch/person \
    --header 'authorization: Bearer YOUR_API_KEY' \
    --header 'content-type: application/json' \
    --header 'x-api-version: 2025-11-01' \
    --data '{
      "entities": {
        "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"]
      },
      "track": { "field": "experience.employment_details.current", "type": "added" },
      "config": {
        "trigger": { "type": "interval", "every_hours": 24 },
        "max_results_per_run": 100
      },
      "notifications": [
        { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }
      ]
    }'
  ```

  ```json Response theme={"theme":"vitesse-black"}
  {
    "id": 46936,
    "kind": "entity",
    "dataset": "person",
    "api_version": "2025-11-01",
    "config_version": "2025-11-01",
    "status": "active",
    "entities": {
      "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"]
    },
    "track": { "field": "experience.employment_details.current", "type": "added" },
    "fields": null,
    "config": {
      "trigger": { "type": "interval", "every_hours": 24 },
      "max_results_per_run": 100,
      "expires_at": null,
      "preferred_hour": null
    },
    "notifications": [
      { "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }
    ],
    "created_at": "2026-07-16T03:16:38.351849Z",
    "last_run_at": null
  }
  ```
</CodeGroup>

## Edit the watched list any time

Unlike a saved search, an Entity Watcher's list is **mutable** — add or remove people
as your pipeline or contact list changes, without recreating the watch. `PATCH` the
watch with a new `entities` object:

<CodeGroup>
  ```bash Request theme={"theme":"vitesse-black"}
  curl --request PATCH \
    --url https://api.crustdata.com/watch/person/46936 \
    --header 'authorization: Bearer YOUR_API_KEY' \
    --header 'content-type: application/json' \
    --header 'x-api-version: 2025-11-01' \
    --data '{
      "entities": {
        "professional_network_profile_urls": [
          "https://www.linkedin.com/in/sherryrobinson",
          "https://www.linkedin.com/in/williamhgates"
        ]
      }
    }'
  ```

  ```json Response theme={"theme":"vitesse-black"}
  {
    "id": 46936,
    "status": "active",
    "entities": {
      "professional_network_profile_urls": [
        "https://www.linkedin.com/in/sherryrobinson",
        "https://www.linkedin.com/in/williamhgates"
      ]
    }
  }
  ```
</CodeGroup>

Newly added people are **baselined silently** on the next run — they establish their
"before" snapshot first, so adding someone never fires a spurious notification. You
can also `PATCH` `status` (`active` / `paused`), `config`, and `notifications`. A
watch's `track` and `fields` are fixed once created; to change what you watch for,
create a new watch.

## Manage your watches

| Action              | Request                                                  |
| ------------------- | -------------------------------------------------------- |
| List your watches   | `GET /watch/person?status=active&limit=50&offset=0`      |
| Get one             | `GET /watch/person/{id}`                                 |
| Pause / resume      | `PATCH /watch/person/{id}` with `{ "status": "paused" }` |
| Add/remove entities | `PATCH /watch/person/{id}` with a new `entities`         |
| Delete              | `DELETE /watch/person/{id}` → `204`                      |

Full request/response for each, verified live.

<AccordionGroup>
  <Accordion title="List your watches — GET /watch/person">
    <CodeGroup>
      ```bash Request theme={"theme":"vitesse-black"}
      curl --request GET \
        --url 'https://api.crustdata.com/watch/person?status=active&limit=50&offset=0' \
        --header 'authorization: Bearer YOUR_API_KEY' \
        --header 'x-api-version: 2025-11-01'
      ```

      ```json Response theme={"theme":"vitesse-black"}
      [
        {
          "id": 46957,
          "kind": "entity",
          "dataset": "person",
          "status": "active",
          "entities": {
            "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"]
          },
          "track": { "type": "added", "field": "experience.employment_details.current" },
          "config": { "trigger": { "type": "interval", "every_hours": 24 }, "max_results_per_run": 25 },
          "notifications": [{ "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }],
          "created_at": "2026-07-16T13:39:18.247195Z",
          "last_run_at": "2026-07-16T13:39:18.598964Z"
        }
      ]
      ```
    </CodeGroup>

    Returns an array of your person watches. Filter with `status`, page with `limit` and `offset`.
  </Accordion>

  <Accordion title="Get one watch — GET /watch/person/{id}">
    <CodeGroup>
      ```bash Request theme={"theme":"vitesse-black"}
      curl --request GET \
        --url https://api.crustdata.com/watch/person/46957 \
        --header 'authorization: Bearer YOUR_API_KEY' \
        --header 'x-api-version: 2025-11-01'
      ```

      ```json Response theme={"theme":"vitesse-black"}
      {
        "id": 46957,
        "kind": "entity",
        "dataset": "person",
        "api_version": "2025-11-01",
        "config_version": "2025-11-01",
        "status": "active",
        "entities": {
          "professional_network_profile_urls": ["https://www.linkedin.com/in/sherryrobinson"]
        },
        "track": { "type": "added", "field": "experience.employment_details.current" },
        "fields": null,
        "config": {
          "trigger": { "type": "interval", "every_hours": 24 },
          "max_results_per_run": 25,
          "expires_at": null,
          "preferred_hour": null
        },
        "notifications": [{ "type": "webhook", "url": "https://your-app.com/webhooks/crustdata" }],
        "created_at": "2026-07-16T13:39:18.247195Z",
        "last_run_at": "2026-07-16T13:39:18.598964Z"
      }
      ```
    </CodeGroup>

    `last_run_at` is `null` until the baseline run completes, then carries the most recent run's timestamp.
  </Accordion>

  <Accordion title="Pause or resume — PATCH status">
    <CodeGroup>
      ```bash Request theme={"theme":"vitesse-black"}
      curl --request PATCH \
        --url https://api.crustdata.com/watch/person/46957 \
        --header 'authorization: Bearer YOUR_API_KEY' \
        --header 'content-type: application/json' \
        --header 'x-api-version: 2025-11-01' \
        --data '{ "status": "paused" }'
      ```

      ```json Response theme={"theme":"vitesse-black"}
      { "id": 46957, "status": "paused" }
      ```
    </CodeGroup>

    A paused watch stops running until you resume it. Send `{ "status": "active" }` to resume.
  </Accordion>

  <Accordion title="Delete a watch — DELETE /watch/person/{id}">
    ```bash Request theme={"theme":"vitesse-black"}
    curl --request DELETE \
      --url https://api.crustdata.com/watch/person/46957 \
      --header 'authorization: Bearer YOUR_API_KEY' \
      --header 'x-api-version: 2025-11-01'
    ```

    Returns `204 No Content`. Deletion is terminal — the watch cannot be resumed.
  </Accordion>
</AccordionGroup>

## What a notification looks like

Every fired person carries two things: the **`changes`** array (exactly what moved,
independent of `fields`) and a **`record`** (the person's current data projected to
the `fields` you requested — the same shape the Enrich API returns, so with default
`fields` it's just `basic_profile` + `social_handles`). The person's identity travels
**inside** the record as `crustdata_person_id`.

Two ways to receive these, each with a **different envelope**:

### Pushed to your webhook

When a watch fires, we `POST` this body to each channel. The example below is from a
watch created with `fields: ["basic_profile", "experience"]`:

```json Webhook POST body theme={"theme":"vitesse-black"}
{
  "metadata": {
    "watch_id": 46936,
    "kind": "entity",
    "dataset": "person",
    "api_version": "2025-11-01",
    "run_id": 64200,
    "notification_id": "ntf_64200",
    "delivered_at": "2026-07-16T03:20:00.000000Z",
    "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 100, "truncated": false }
  },
  "results": [
    {
      "changes": [
        {
          "field": "experience.employment_details.current",
          "type": "added",
          "new_elements": [ { "title": "…", "name": "…", "start_date": "…" } ]
        }
      ],
      "record": {
        "basic_profile": { "name": "…", "current_title": "…", "headline": "…" },
        "experience": { "employment_details": { "current": [ { "title": "…", "name": "…", "start_date": "…" } ] } },
        "crustdata_person_id": 6324687
      }
    }
  ]
}
```

For an entity watch, `results` is a **flat list** — one object per fired person, each
with its own `changes` and `record`. A `changed` scalar reports as
`{ "field": …, "type": "changed", "from": …, "to": … }`; an `added` array as
`{ "field": …, "type": "added", "new_elements": [ … ] }`.

### Pulled from run history

To re-read a past run — or audit exactly what was delivered — the run-history
endpoint returns the same content under a different envelope.

```json GET /watcher/watches/{id}/runs/{run_id}/summary theme={"theme":"vitesse-black"}
{
  "id": 64200,
  "status": "SUCCESS",
  "new_records_count": 1,
  "notifications": [
    {
      "sent_at": "2026-07-16T03:20:00Z",
      "http_status": 200,
      "payload": {
        "subscription_id": 46936,
        "event_type": "indb:entity:person",
        "timestamp": "2026-07-16T03:20:00Z",
        "notifications": [
          {
            "uid": "person_6324687_64200",
            "changes": [ { "field": "experience.employment_details.current", "type": "added", "new_elements": [ { … } ] } ],
            "record": { "basic_profile": { … }, "experience": { … }, "crustdata_person_id": 6324687 }
          }
        ]
      }
    }
  ]
}
```

## Rate limits

Watch-management requests are rate-limited to **10 requests per minute** per API key.
This bounds bursty create/update loops; steady use is unaffected.

<CardGroup cols={2}>
  <Card title="Person Discovery Watcher" icon="radar" href="/watcher-docs/person/discovery">
    Need to *find* new matching people instead of watching a known list? Use the
    Person Discovery Watcher.
  </Card>

  <Card title="Person field reference" icon="user" href="/person-docs/enrichment/reference">
    Browse the full set of trackable `field` paths for people.
  </Card>
</CardGroup>
