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.
Authorizations
API key passed as a Bearer token in the Authorization header.
Headers
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.
2025-11-01 "2025-11-01"
Path Parameters
The dataset the watch is built on.
person, company, job, social_post "person"
Body
Body for creating a discovery watch. The dataset comes from the path, and the API version from
the x-api-version header.
filters is required on an ordinary watch. On a realtime watch (config.is_realtime: true)
realtime_filters is what the watch runs on, narrowing_filters narrows what it returned,
and filters is refused.
Schedule and per-run caps for a discovery watch.
Ask what this watch would do instead of creating it. The request is validated exactly as a create is, nothing is persisted, and no credits are charged or reserved — so a preview works even on an account with no credits left, which is the balance a preview is usually being run to inform.
Answers 200 with the preview rather than 201 with a watch. Everything else in the body
means the same thing it means on a create, so preview and create differ by this flag alone.
true
The search filter to re-run each interval. A single leaf or a nested AND/OR group, in the dataset's full search grammar.
Required on an index watch, and refused on a realtime one. A realtime watch's records
are new by construction, so asking this tree about one means asking an index that has
never seen it, which drops every result. Narrow a realtime watch with
narrowing_filters instead.
- Option 1
- Option 2
What the live search is asked for on a realtime watch. Requires config.is_realtime: true, and sending one without the other is a 400 in both directions.
This tree accepts fewer fields and a simpler shape than the dataset's full search
grammar. Each leaf follows the leaf schema for the path's dataset:
RealtimeConditionPerson, RealtimeConditionCompany, RealtimeConditionJob, or
RealtimeConditionSocialPost. Each one lists the fields that dataset's live search
accepts and the operators and values each field takes. The tree shape is in
RealtimeConditionGroup.
Fields the live search cannot express belong in narrowing_filters, when the
dataset that owns them is one this watch may narrow on; the refusal names the
block. Values are unchanged: you write the same ones here as you do on an index
watch, and the watch translates them for the live search.
- Option 1
- Option 2
- Option 3
- Option 4
- Option 5
What narrows a realtime watch, keyed by dataset. Requires config.is_realtime: true,
and goes at the top level of the body rather than inside config.
A record the live search just found is too new for its own index to hold, so narrowing asks about the durable entity behind it instead: the author of a post, the company hiring for a job, the person as the index already knows them. Each block is a plain single-dataset tree asked of that dataset's own index, in that dataset's own vocabulary, so a field belonging to another dataset is refused rather than resolved.
A block keeps that dataset's full search grammar, unlike realtime_filters: an
or across two different fields, nested and / or groups, and every negation
operator all work, because an index answers it rather than the live search. The one
restriction is that every leaf must belong to the block's own dataset. The fields and
operators are exactly those of that dataset's search endpoint: PersonSearchCondition
on POST /person/search for the person block, and SearchCondition on
POST /company/search for the company block.
Only person and company may be named, and which of the two depends on the watch:
a person or social_post watch may narrow on either, and a company or job watch
on company alone. Name one block. A job posting or a social post is the new record
itself, so there is no older copy of it to ask about, and a condition about the record
the search returned belongs in realtime_filters.
A record whose entity the index does not hold is dropped rather than delivered
unchecked. Immutable once set, like realtime_filters.
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.
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.
Which membership transitions notify you. Only added, records that newly match, is
supported today, and it is the default.
added Delivery channels. May be empty for a pull-only watch, which pushes nothing and whose results you read from the run-history endpoints instead.
One delivery channel. Every channel on a watch receives every notification.
- Option 1
- Option 2
- Option 3
- Option 4
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.
redeliver, drop "redeliver"
Response
The preview, when the body carried preview: true. Nothing was created and nothing was
charged.
What a discovery watch would match right now. can_create is stated rather than implied by an
empty error, so a caller does not have to infer validity from an absence.
Whether this body would be accepted by a create. Every check a create runs has already run,
so true means the create will pass and false names the reason in error.
true
How many records match right now. The first run marks this whole set seen — permanently —
and delivers at most 5 of it, so a broad filter spends its backlog on run one. Narrow it
before arming rather than after. null when the index could not be reached.
1240
Up to three matching records, each cut to the identity a delivered notification keeps.
Where the count came from, not which tier the watch will run on. A realtime watch is previewed against the index too, because sampling live means running the live retrieval the watch exists to pay for, so its preview is an illustration rather than a forecast: staler, and broader wherever a live-only field has no index column.
"index"
What this watch will be charged, at the settings it was asked about.
Why the watch cannot be created, when can_create is false.
The body field the error is attached to, when it belongs to one.

