> ## 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 per notification — one charge for each watched person that
  changed on a run. A run that surfaces nothing costs nothing, and the rate does
  **not** depend on how often you check (`every_hours`) — only on the
  [data-freshness tier](#data-freshness) you pick (<strong>5 credits</strong> at the
  default 30-day freshness). 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 — you're charged per notification at your chosen
[data-freshness tier](#data-freshness) (5 credits at the default), no matter 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. The delivered `record` uses the **same schema as [Person Enrich](/person-docs/enrichment/reference)** — identical field-group names and nesting — so the example below mirrors the full Person Enrich `fields` set.

```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).                                                                                                                        |
| `refresh_frequency_days` | no       | How fresh to keep the tracked data, in days (1–30, default 30). Fewer days means fresher data and a higher per-notification rate — see [Data freshness](#data-freshness). |
| `preferred_hour`         | no       | Hour of day (0–23) to prefer for the run.                                                                                                                                 |
| `expires_at`             | no       | Auto-expire date, `YYYY-MM-DD`.                                                                                                                                           |

## Data freshness

`config.refresh_frequency_days` sets **how fresh the tracked data is kept** — how
recently each tracked field is refreshed before a run compares it against the last
snapshot. It is independent of `trigger.every_hours`: `every_hours` is how often the
watch *checks and notifies*; `refresh_frequency_days` is how up-to-date the data it
checks against is, which is what decides how quickly a real change is detected.

Fresher data is priced higher — the per-notification charge is tiered by the freshness
you choose:

| `refresh_frequency_days` | Credits per changed person |
| ------------------------ | -------------------------- |
| not set / `30` (default) | 5                          |
| `14`                     | 10                         |
| `7`                      | 20                         |
| `3`                      | 50                         |
| `1`                      | 150                        |

The accepted range is **1–30**. A value between two tiers is billed at the fresher
(more expensive) tier. Only fields backed by a refresh asset can be kept fresh this
way; if a tracked field has no backing asset, the create is rejected — drop that field
or the cadence.

## 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 }
          }
        ]
      }
    }
  ]
}
```

## Smoke-test your webhook

Before a real change ever fires, push **one sample notification** through a watch to
verify your receiver end-to-end — signature check, JSON parsing, routing — without
waiting for a person to actually move. The test delivers the **exact envelope a real
run sends** (same shape, same signature headers), flagged with `metadata.test: true`.
It runs no diff, records no snapshot, **costs no credits**, and persists nothing.

```
POST https://api.crustdata.com/watch/person/{watch_id}/test
```

All three body fields are optional:

| Field                   | Type      | Description                                                                                                                                                                                                                                          |
| ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notification_endpoint` | string    | Deliver this one test to a URL you control instead of the watch's configured channels — handy with a request-inspection service. Must be a **public** `http(s)` URL; endpoints resolving to private, loopback, or link-local addresses are rejected. |
| `fields`                | string\[] | Project the sample `record` to a specific field set for this test (defaults to the watch's own `fields`).                                                                                                                                            |
| `count`                 | integer   | Deliver `N` sample people (`1`–`25`) in one envelope to exercise multi-record handling. Each gets a distinct `crustdata_person_id`. Default `1`.                                                                                                     |

This request and response are real:

<CodeGroup>
  ```bash Request theme={"theme":"vitesse-black"}
  curl --request POST \
    --url https://api.crustdata.com/watch/person/47356/test \
    --header 'authorization: Bearer YOUR_API_KEY' \
    --header 'content-type: application/json' \
    --header 'x-api-version: 2025-11-01' \
    --data '{
      "notification_endpoint": "https://your-server.example.com/webhook",
      "fields": ["basic_profile", "experience"]
    }'
  ```

  ```json Response theme={"theme":"vitesse-black"}
  {
    "delivered": [
      {
        "type": "webhook",
        "endpoint": "https://your-server.example.com/webhook",
        "http_status": 200,
        "ok": true,
        "response": "…your receiver's response body, truncated to 500 chars…"
      }
    ],
    "envelope": { "…the exact body delivered to your webhook, shown below…" }
  }
  ```
</CodeGroup>

Your receiver gets this `POST`. It is the shape of a real notification — only
`metadata.test: true` and the **sample values** (`"sample_text"`, `1234`, …) mark it
as a test. The `changes` array below is abbreviated to two entries; the full array
mirrors your watch's `track`:

```http Request headers theme={"theme":"vitesse-black"}
Content-Type: application/json
x-api-version: 2025-11-01
x-crustdata-watch-id: 47356
x-crustdata-event-id: ntf_test_1785211467
x-crustdata-signature: t=1785211468,v1=<hex-hmac-sha256>
```

```json Webhook POST body theme={"theme":"vitesse-black"}
{
  "metadata": {
    "watch_id": 47356,
    "kind": "entity",
    "dataset": "person",
    "api_version": "2025-11-01",
    "run_id": 1785211467,
    "notification_id": "ntf_test_1785211467",
    "delivered_at": "2026-07-28T04:04:27.667425Z",
    "summary": { "delivered": 1, "total_count": 1, "max_results_per_run": 1000, "truncated": false },
    "test": true
  },
  "results": [
    {
      "changes": [
        {
          "field": "experience.employment_details.current",
          "type": "added",
          "new_elements": [
            {
              "name": "sample_text",
              "title": "sample_text",
              "start_date": "sample_text",
              "is_default": true,
              "crustdata_company_id": 1234,
              "seniority_level": "sample_text"
            }
          ]
        },
        { "field": "basic_profile.current_title", "type": "changed", "value": null, "from": null, "to": "sample_text" }
      ],
      "record": {
        "crustdata_person_id": 6324687,
        "basic_profile": {
          "name": "sample_text",
          "headline": "sample_text",
          "first_name": "sample_text",
          "last_name": "sample_text",
          "current_title": "sample_text",
          "summary": "sample_text",
          "location": { "city": "sample_text", "state": "sample_text", "country": "sample_text" }
        },
        "experience": {
          "employment_details": {
            "current": [
              { "name": "sample_text", "title": "sample_text", "start_date": "sample_text", "is_default": true }
            ],
            "past": [
              { "name": "sample_text", "title": "sample_text", "start_date": "sample_text", "end_date": "sample_text" }
            ]
          }
        }
      }
    }
  ]
}
```

<Note>
  The sample `record` carries **placeholder values** with the exact **shape** of a
  real notification (every field in the `fields` groups you requested is present), so
  your parser, signature check, and routing see production structure — just not real
  person data. A `changed` scalar reports `{ "type": "changed", "value": …, "from": …,
      "to": … }` (`value` is the threshold target for `>`/`<` operators, `null` for a plain
  `changed`); an `added` array reports `{ "type": "added", "new_elements": [ … ] }`.
</Note>

Pass `"count": 3` to receive three sample people in one envelope — distinct
`crustdata_person_id`s and `summary.delivered: 3` — so you can exercise how your
receiver iterates `results`.

<Note>
  The signature is computed exactly as for a real notification — HMAC-SHA256 over
  `<timestamp>.<raw-body>` keyed by your API key, delivered as
  `x-crustdata-signature: t=<unix>,v1=<hex>`. A test that verifies here verifies real
  deliveries.
</Note>

## 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>


## Related topics

- [Pricing](/general/pricing.md)
- [Person Discovery Watcher](/watcher-docs/person/discovery.md)
- [Company Entity Watcher](/watcher-docs/company/entity.md)
- [Company Discovery Watcher](/watcher-docs/company/discovery.md)
- [Job Watcher](/watcher-docs/job/watch.md)
