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

# Create a discovery watch

> Creates a discovery watch: a saved search filter that re-runs on an interval and delivers the
records that newly match it.

The filter uses the same syntax and the same field names as the dataset's own search endpoint,
so there is no second filter language. A leaf is `{ "field", "type", "value" }` and groups
combine leaves with `{ "op": "and" | "or", "conditions": [...] }`. Groups nest.

The first run starts within seconds and is a free baseline capped at 5 records, so you can
confirm the delivery path and the payload shape before any credit is spent. Later runs deliver
up to `config.max_results_per_run` records each and are charged per record delivered.

A watch whose filter matches more than 500,000 records is rejected at creation: narrow the
filter. That bound is on the baseline, the set the free first run enumerates and records as
delivered, not on how many records the watch may deliver over its lifetime; later runs are
capped by `config.max_results_per_run`. A filter that names a field from another dataset is a
valid cross-dataset condition
rather than a typo, but row-level negation (`!=`, `not_in`, `(!)`, `geo_exclude`) is rejected
there because it matches subjects with *some other* matching row rather than subjects with
none.




## OpenAPI

````yaml /openapi-specs/2025-11-01/watch.yaml post /watch/{dataset}/search
openapi: 3.0.3
info:
  title: Watch API
  version: '2025-11-01'
  description: >
    The Watch API turns a Crustdata query into a recurring feed. A watch runs on
    your schedule and

    delivers only what is new or what changed since its previous run, to a
    webhook, Slack, Google Chat,

    or email.


    There are two kinds of watch, and the kind decides the URL:


    - **Discovery watches** (`/watch/{dataset}/search`) re-run a *search filter*
    and deliver records that
      newly match it. Use one to find people, companies, or jobs you do not know yet. Available for
      `person`, `company`, and `job`.
    - **Entity watches** (`/watch/{dataset}`) monitor a *list you supply* and
    deliver a notification when
      a tracked field on one of those records changes. Use one to follow a known set. Available for
      `person` and `company`.

    Both kinds share the same `config`, `notifications`, and run-history
    surfaces, and both start with a

    free baseline run: a discovery watch delivers a sample of up to 5 current
    matches, an entity watch

    records each subject's starting values and never fires. Every request needs
    an `Authorization` Bearer

    API key and the `x-api-version: 2025-11-01` header.


    **Credits.** A discovery watch is charged per new record delivered: 0.5 for
    a person, 2 for a company,

    0.5 for a job. An entity watch is charged per notification, tiered by
    `config.refresh_frequency_days`:

    5 credits at the 30-day default, 10 at 14 days, 20 at 7 days, 50 at 3 days,
    150 at 1 day. A run that

    finds nothing costs nothing, and the baseline run is free for both kinds.


    **Rate limit.** Watch-management requests are limited to 10 requests per
    minute per API key.


    **Access.** Creating a watch is open to any authenticated API customer, and
    the 10-requests-per-minute

    limit is what bounds abuse. What is gated is the data underneath: a create
    is rejected with `403` when

    the key is not entitled to the dataset, or when `fields` or `track` name
    field groups the key may not

    receive. Contact info@crustdata.com to request access.
servers:
  - url: https://api.crustdata.com
    description: Production API server
security:
  - bearerAuth: []
tags:
  - name: Watch APIs
    description: Recurring feeds over the Crustdata datasets
  - name: Discovery Watches
    description: Watches that re-run a search filter and deliver newly matching records
  - name: Entity Watches
    description: >-
      Watches that monitor a supplied list of people or companies for field
      changes
  - name: Watch Runs
    description: Run history and per-run delivery detail, shared by both watch kinds
paths:
  /watch/{dataset}/search:
    post:
      tags:
        - Watch APIs
        - Discovery Watches
      summary: Create a discovery watch
      description: >
        Creates a discovery watch: a saved search filter that re-runs on an
        interval and delivers the

        records that newly match it.


        The filter uses the same syntax and the same field names as the
        dataset's own search endpoint,

        so there is no second filter language. A leaf is `{ "field", "type",
        "value" }` and groups

        combine leaves with `{ "op": "and" | "or", "conditions": [...] }`.
        Groups nest.


        The first run starts within seconds and is a free baseline capped at 5
        records, so you can

        confirm the delivery path and the payload shape before any credit is
        spent. Later runs deliver

        up to `config.max_results_per_run` records each and are charged per
        record delivered.


        A watch whose filter matches more than 500,000 records is rejected at
        creation: narrow the

        filter. That bound is on the baseline, the set the free first run
        enumerates and records as

        delivered, not on how many records the watch may deliver over its
        lifetime; later runs are

        capped by `config.max_results_per_run`. A filter that names a field from
        another dataset is a

        valid cross-dataset condition

        rather than a typo, but row-level negation (`!=`, `not_in`, `(!)`,
        `geo_exclude`) is rejected

        there because it matches subjects with *some other* matching row rather
        than subjects with

        none.
      operationId: createDiscoveryWatch
      parameters:
        - $ref: '#/components/parameters/DatasetPath'
        - $ref: '#/components/parameters/ApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DiscoveryWatchCreateRequest'
            examples:
              person_persona_and_location:
                summary: 'person: new ML engineers in Paris, checked daily'
                value:
                  filters:
                    op: and
                    conditions:
                      - field: experience.employment_details.current.title
                        type: (.)
                        value: Machine Learning Engineer
                      - field: professional_network.location.raw
                        type: (.)
                        value: Paris
                  config:
                    trigger:
                      type: interval
                      every_hours: 24
                    max_results_per_run: 25
                  notifications:
                    - type: webhook
                      url: https://your-app.com/webhooks/crustdata
              company_growth_band:
                summary: 'company: new accounts entering an ICP, checked weekly'
                value:
                  filters:
                    op: and
                    conditions:
                      - field: headcount.total
                        type: '=>'
                        value: 51
                      - field: headcount.total
                        type: '=<'
                        value: 500
                  config:
                    trigger:
                      type: interval
                      every_hours: 168
                  notifications:
                    - type: slack
                      webhook_url: https://hooks.slack.com/services/T000/B000/XXXX
              job_recent_postings:
                summary: 'job: new engineering roles, newest first'
                value:
                  filters:
                    op: and
                    conditions:
                      - field: job_details.title
                        type: (.)
                        value: software engineer
                      - field: location.country
                        type: '='
                        value: United States
                      - field: metadata.date_added
                        type: '=>'
                        value: '2026-07-05'
                  sorts:
                    - field: metadata.date_added
                      order: desc
                  config:
                    trigger:
                      type: interval
                      every_hours: 24
                  notifications:
                    - type: webhook
                      url: https://your-app.com/webhooks/crustdata
              company_cross_dataset:
                summary: >-
                  company: cross-dataset, companies on Snowflake hiring a data
                  engineer
                value:
                  filters:
                    op: and
                    conditions:
                      - field: technographics.technologies.name
                        type: '='
                        value: Snowflake
                      - field: job_details.title
                        type: (.)
                        value: data engineer
                  config:
                    trigger:
                      type: interval
                      every_hours: 24
                  notifications:
                    - type: webhook
                      url: https://your-app.com/webhooks/crustdata
              pull_only:
                summary: 'no channel: read results from the runs endpoints instead'
                value:
                  filters:
                    field: job_details.category
                    type: '='
                    value: Engineering
                  config:
                    trigger:
                      type: interval
                      every_hours: 24
                  notifications: []
      responses:
        '201':
          description: The watch was created and its baseline run was scheduled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoveryWatch'
        '400':
          description: >
            The body failed validation, or `x-api-version` is missing or
            unsupported. The message

            names the offending path, for example `filters.conditions[1] is
            missing 'value'.`
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SimpleErrorResponse'
                  - $ref: '#/components/schemas/ValidationErrorResponse'
              examples:
                missing_api_version:
                  summary: No x-api-version header
                  value:
                    error: 'Missing required header: x-api-version'
                unknown_filter_field:
                  summary: A filter names a field no dataset defines
                  value:
                    non_field_errors:
                      - >-
                        Unknown filter field at filters.conditions[0]:
                        'current_title'.
                too_broad:
                  summary: The filter matches more records than a watch may track
                  value:
                    error: >-
                      This watch would follow 1,204,338 records, more than the
                      500,000 a single watch can track. Please narrow your
                      filters.
                cross_dataset_negation:
                  summary: A negating operator on a filter from another dataset
                  value:
                    non_field_errors:
                      - >-
                        filters.conditions[1]: '(!)' on a job filter matches any
                        company with some *other* matching job record, not those
                        with none. List the values you do want instead.
                cross_dataset_too_broad:
                  summary: A filter from another dataset covers too many companies
                  value:
                    error: >-
                      Your company filters cover more than 1,000,000 companies.
                      Add another company filter — a location, a date, a more
                      specific title — so this watch follows a smaller set of
                      companies.
                cross_dataset_unavailable:
                  summary: The other dataset could not be reached at creation time
                  value:
                    error: >-
                      We could not check your job filters just now. Please try
                      again in a few minutes.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >
            The key is not entitled to the dataset the watch reads, or the
            requested `fields`

            include groups the key may not receive.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SimpleErrorResponse'
                  - $ref: '#/components/schemas/FieldAccessErrorResponse'
              examples:
                dataset_not_granted:
                  summary: The key cannot watch this dataset
                  value:
                    error: >-
                      You do not have access to the person dataset. Contact
                      info@crustdata.com to request access.
                fields_denied:
                  summary: A requested field group is not enabled on the key
                  value:
                    error: 'Access denied to fields: contact'
                    denied_fields:
                      - contact
                    permitted_fields:
                      - basic_profile
                      - experience
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The watch could not be created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
              example:
                error: Error creating watch! Contact info@crustdata.com
components:
  parameters:
    DatasetPath:
      name: dataset
      in: path
      required: true
      description: The dataset the watch is built on.
      schema:
        type: string
        enum:
          - person
          - company
          - job
        example: person
    ApiVersion:
      name: x-api-version
      in: header
      required: true
      schema:
        type: string
        enum:
          - '2025-11-01'
        default: '2025-11-01'
        example: '2025-11-01'
      description: >
        API version to use. `2025-11-01` is the only accepted value. Send it on
        every call: it is

        enforced today on `POST /watch/{dataset}/search`, where a missing or
        unrecognized value returns

        `400`, and the other watch routes accept a request without it rather
        than relying on that.
  schemas:
    DiscoveryWatchCreateRequest:
      type: object
      description: >-
        Body for creating a discovery watch. The dataset comes from the path,
        and the API version from the `x-api-version` header.
      required:
        - filters
        - config
      properties:
        filters:
          description: >-
            The search filter to re-run each interval. A single leaf or a nested
            AND/OR group.
          oneOf:
            - $ref: '#/components/schemas/FilterCondition'
            - $ref: '#/components/schemas/FilterConditionGroup'
        sorts:
          type: array
          description: >
            Delivery order within a run, for example newest first. Sorting does
            not change which

            records are new, only the order they arrive in when a run is capped.
          items:
            $ref: '#/components/schemas/SortSpec'
        fields:
          type: array
          description: >
            Field groups or field paths to include in each delivered `record`.
            Defaults to the

            dataset's standard projection. Every requested field is checked
            against the key's

            entitlements at creation, and `fields` does not affect what a record
            costs.
          items:
            type: string
          example:
            - basic_profile
            - experience
        'on':
          type: array
          description: >
            Which membership transitions notify you. Only `added`, records that
            newly match, is

            supported today, and it is the default.
          items:
            type: string
            enum:
              - added
          default:
            - added
          example:
            - added
        config:
          $ref: '#/components/schemas/DiscoveryWatchConfig'
        notifications:
          type: array
          description: >
            Delivery channels. May be empty for a pull-only watch, which pushes
            nothing and whose

            results you read from the run-history endpoints instead.
          items:
            $ref: '#/components/schemas/NotificationChannel'
        overflow_policy:
          type: string
          enum:
            - redeliver
            - drop
          default: redeliver
          description: >
            What happens to new records beyond `max_results_per_run`.
            `redeliver` leaves the

            overflow unseen so it comes back in a later run. `drop` marks every
            new record seen so

            the overflow never returns.
          example: redeliver
    DiscoveryWatch:
      type: object
      description: A discovery watch, as returned by create, get, list, and update.
      properties:
        id:
          type: integer
          description: The watch's id. Use it on every management and run-history path.
          example: 47609
        kind:
          type: string
          enum:
            - discovery
          example: discovery
        dataset:
          type: string
          enum:
            - person
            - company
            - job
          example: job
        api_version:
          type: string
          example: '2025-11-01'
        config_version:
          type: string
          description: Version of the stored config shape.
          example: '2025-11-01'
        status:
          type: string
          enum:
            - active
            - paused
            - suspended
            - expired
            - cancelled
          description: >
            Lifecycle state. `suspended` is set by the system, most often for an
            insufficient

            balance, and blocks status changes until it is resolved.
          example: active
        filters:
          description: The saved filter, as sent at creation.
          oneOf:
            - $ref: '#/components/schemas/FilterCondition'
            - $ref: '#/components/schemas/FilterConditionGroup'
        'on':
          type: array
          items:
            type: string
          example:
            - added
        sorts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/SortSpec'
        fields:
          type: array
          nullable: true
          items:
            type: string
        config:
          $ref: '#/components/schemas/DiscoveryWatchConfig'
        notifications:
          type: array
          description: The configured channels. A stored channel secret is never returned.
          items:
            $ref: '#/components/schemas/NotificationChannel'
        created_at:
          type: string
          format: date-time
          example: '2026-07-16T03:16:38.351849Z'
        last_run_at:
          type: string
          format: date-time
          nullable: true
          description: When the watch last ran. `null` until the baseline run completes.
          example: '2026-08-04T05:32:47.000000Z'
        notifications_count:
          type: integer
          description: All-time count of records this watch has delivered.
          example: 128
        credits_consumed:
          type: number
          description: All-time credits this watch has spent.
          example: 64
        last_notified_at:
          type: string
          format: date-time
          nullable: true
          description: When this watch last delivered anything.
          example: '2026-08-04T05:32:47.000000Z'
    SimpleErrorResponse:
      type: object
      description: Error payload with a single human-readable message.
      required:
        - error
      example:
        error: Watch not found
      properties:
        error:
          type: string
          description: Human-readable description of what went wrong.
          example: Watch not found
    ValidationErrorResponse:
      type: object
      description: >
        Field-level validation errors, keyed by the request field that failed. A
        whole-body rule

        reports under `non_field_errors`. The message names the offending path
        inside a filter or track

        tree, for example `filters.conditions[1] is missing 'value'.`
      additionalProperties:
        type: array
        items:
          type: string
      example:
        non_field_errors:
          - 'Unknown filter operator at filters.conditions[0]: ''contains''.'
    FieldAccessErrorResponse:
      type: object
      description: >-
        A requested field group is not enabled on the key. The response names
        both what was denied and what is permitted.
      required:
        - error
      properties:
        error:
          type: string
          example: 'Access denied to fields: contact'
        denied_fields:
          type: array
          items:
            type: string
          example:
            - contact
        permitted_fields:
          type: array
          items:
            type: string
          example:
            - basic_profile
            - experience
    FilterCondition:
      type: object
      description: >
        One filter leaf. `field` and the operators are exactly those of the
        dataset's own search

        endpoint, so the Person, Company, and Job search references are the full
        catalog.
      required:
        - field
        - type
      properties:
        field:
          type: string
          description: >
            Dot-path of the field to filter on, for example

            `experience.employment_details.current.title` for a person or
            `job_details.title` for a

            job. Field names are unique across datasets, so a leaf naming
            another dataset's field is

            a valid cross-dataset condition rather than a mistake.
          example: experience.employment_details.current.title
        type:
          type: string
          description: >
            The comparison operator. Crustdata writes greater-or-equal as `=>`
            and less-or-equal as

            `=<`, not `>=` / `<=`.


            A cross-dataset leaf may not use a row-level negation operator
            (`!=`, `not_in`, `(!)`,

            `geo_exclude`): it would match subjects with some *other* matching
            row rather than

            subjects with none. List the values you do want instead.
          example: (.)
          enum:
            - '='
            - '!='
            - in
            - not_in
            - '>'
            - <
            - '=>'
            - '=<'
            - (.)
            - (!)
            - '[.]'
            - geo_distance
            - geo_exclude
        value:
          description: >
            The value to compare against. A scalar for `=`, `!=`, `>`, `<`,
            `=>`, `=<`, and the

            substring operators; an array for `in` and `not_in`; an object for
            the geo operators.

            Required for every operator except the null checks.
          example: Machine Learning Engineer
    FilterConditionGroup:
      type: object
      description: >-
        A group that combines conditions with AND or OR. Groups nest, so an
        `and` group can sit inside an `or` group's `conditions`.
      required:
        - op
        - conditions
      properties:
        op:
          type: string
          enum:
            - and
            - or
          description: How to combine `conditions`.
          example: and
        conditions:
          type: array
          minItems: 1
          description: The conditions to combine. Each item is a leaf or another group.
          items:
            oneOf:
              - $ref: '#/components/schemas/FilterCondition'
              - $ref: '#/components/schemas/FilterConditionGroup'
    SortSpec:
      type: object
      description: >-
        One sort key. Only indexed, sortable fields of the dataset may be sorted
        on; anything else returns `400` at create time.
      required:
        - field
        - order
      properties:
        field:
          type: string
          description: Dot-path of the field to sort by.
          example: metadata.date_added
        order:
          type: string
          enum:
            - asc
            - desc
          description: Sort direction.
          example: desc
    DiscoveryWatchConfig:
      type: object
      description: Schedule and per-run caps for a discovery watch.
      required:
        - trigger
      properties:
        trigger:
          $ref: '#/components/schemas/WatchTrigger'
        max_results_per_run:
          type: integer
          minimum: 1
          maximum: 1000
          default: 25
          description: >
            Most records to deliver in one run. The first, baseline run is
            always capped at 5

            regardless of this value.
          example: 25
        preferred_hour:
          type: integer
          minimum: 0
          maximum: 23
          nullable: true
          description: Hour of day, in UTC, to prefer for the run.
          example: 9
        expires_at:
          type: string
          format: date
          nullable: true
          description: Auto-expire date, `YYYY-MM-DD`. The watch stops running after it.
          example: '2027-01-01'
    NotificationChannel:
      description: >-
        One delivery channel. Every channel on a watch receives every
        notification.
      oneOf:
        - $ref: '#/components/schemas/WebhookChannel'
        - $ref: '#/components/schemas/SlackChannel'
        - $ref: '#/components/schemas/GoogleChatChannel'
        - $ref: '#/components/schemas/EmailChannel'
      discriminator:
        propertyName: type
        mapping:
          webhook:
            $ref: '#/components/schemas/WebhookChannel'
          slack:
            $ref: '#/components/schemas/SlackChannel'
          google_chat:
            $ref: '#/components/schemas/GoogleChatChannel'
          email:
            $ref: '#/components/schemas/EmailChannel'
    AuthErrorResponse:
      type: object
      description: >
        Authentication error payload. The message distinguishes the two
        failures: `Missing API key in

        request` when no `Authorization` header was sent, and `Invalid API key
        in request` when the

        Bearer token is not a valid key.
      required:
        - message
      example:
        message: Invalid API key in request
      properties:
        message:
          type: string
          description: Human-readable authentication error message.
          example: Invalid API key in request
    StructuredErrorResponse:
      type: object
      description: >-
        Structured error payload, used for rate-limit and API-version failures.
        The flatter `SimpleErrorResponse` shape is used elsewhere, so parse
        defensively.
      required:
        - error
      example:
        error:
          type: rate_limit_error
          message: >-
            Rate limit exceeded for this endpoint. Please write to
            gtm@crustdata.co.
          metadata: []
      properties:
        error:
          type: object
          required:
            - type
            - message
          properties:
            type:
              type: string
              description: Machine-readable error type identifier.
              example: rate_limit_error
            message:
              type: string
              description: Human-readable description of what went wrong.
              example: >-
                Rate limit exceeded for this endpoint. Please write to
                gtm@crustdata.co.
            metadata:
              type: array
              description: Extra context, empty on most errors.
              items:
                type: object
                additionalProperties: true
              example: []
    WatchTrigger:
      type: object
      description: How often the watch runs. Interval is the only trigger type today.
      required:
        - type
        - every_hours
      properties:
        type:
          type: string
          enum:
            - interval
          description: Trigger type.
          example: interval
        every_hours:
          type: integer
          minimum: 1
          description: Hours between runs, for example `1`, `6`, `24`, or `168`.
          example: 24
    WebhookChannel:
      type: object
      description: >-
        An HTTPS endpoint you control. Deliveries are signed, and a URL that
        resolves to a private, loopback, or link-local address is rejected.
      required:
        - type
        - url
      properties:
        type:
          type: string
          enum:
            - webhook
          example: webhook
        url:
          type: string
          format: uri
          description: Public `http(s)` URL to POST each notification to.
          example: https://your-app.com/webhooks/crustdata
        headers:
          type: object
          additionalProperties:
            type: string
          description: >-
            Extra headers to send with each delivery, for routing or
            authentication on your side.
          example:
            X-Watch-Name: ml-engineers-paris
    SlackChannel:
      type: object
      description: >-
        A Slack incoming webhook. The URL must be a genuine
        `https://hooks.slack.com/services/...` webhook; any other URL fails
        delivery.
      required:
        - type
        - webhook_url
      properties:
        type:
          type: string
          enum:
            - slack
          example: slack
        webhook_url:
          type: string
          format: uri
          example: https://hooks.slack.com/services/T000/B000/XXXX
    GoogleChatChannel:
      type: object
      description: A Google Chat incoming webhook.
      required:
        - type
        - url
      properties:
        type:
          type: string
          enum:
            - google_chat
          example: google_chat
        url:
          type: string
          format: uri
          example: https://chat.googleapis.com/v1/spaces/AAAA/messages?key=...
    EmailChannel:
      type: object
      description: One or more email recipients.
      required:
        - type
        - to
      properties:
        type:
          type: string
          enum:
            - email
          example: email
        to:
          type: array
          minItems: 1
          items:
            type: string
            format: email
          description: Recipient addresses.
          example:
            - you@company.com
  responses:
    Unauthorized:
      description: The API key is missing or not valid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AuthErrorResponse'
          examples:
            invalid_api_key:
              summary: Bearer token present but not a valid key
              value:
                message: Invalid API key in request
            missing_api_key:
              summary: No Authorization header at all
              value:
                message: Missing API key in request
    RateLimited:
      description: >
        Too many watch requests. The per-endpoint limit is 10 requests per
        minute per API key, which is

        what bounds bursty create and update loops. Steady use is unaffected.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/StructuredErrorResponse'
          example:
            error:
              type: rate_limit_error
              message: >-
                Rate limit exceeded for this endpoint. Please write to
                gtm@crustdata.co.
              metadata: []
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key passed as a Bearer token in the Authorization header.

````

## Related topics

- [Cross-dataset filters](/watcher-docs/cross-dataset-filters.md)
- [Update a discovery watch](/api-reference/watch-apis/pause-resume-or-reconfigure-a-discovery-watch.md)
- [List discovery watches](/api-reference/watch-apis/list-your-discovery-watches.md)
- [Cancel a discovery watch](/api-reference/watch-apis/cancel-a-discovery-watch.md)
- [Get a discovery watch](/api-reference/watch-apis/get-one-discovery-watch.md)
