YOUR_API_KEY in each example with your actual API key. All
requests require the x-api-version: 2025-11-01 header.Request body
realtime_filters is what the watch runs on and narrowing_filters narrows the
result.
config
Response body
Create returns the whole watch, so you can confirm what was stored before the baseline run starts.Rate limits and credits
2 credits per new company delivered, or
150 credits at the
1-day SLA. A run that
finds nothing new costs nothing.
For your own price per company, open the
watch builder in the dashboard.- Rate limit: 10 requests per minute on
/watch/company/search. Each watch path keeps its own budget. See Rate limits. - Baseline limit: 500,000 companies. See How a watch runs.
- Watched list: filters are fixed once created. To change what you watch for, create a new watch.
Examples
Cross-dataset filters
Realtime watches
realtime_filters.Company Search reference
field, operator, and value.Your first watch: new companies entering a segment
The simplest watch is a search filter plus a schedule and one channel. This one delivers US software companies that newly cross 200 employees, checked once a day.Understanding the response
id: the handle for everything else. Pause, reconfigure, cancel, and read run history all take it.status:activethe moment the watch is created. A run issuspendedby the system rather than half-delivered when your balance is too low.last_run_at:nulluntil the baseline run completes, usually within seconds.filters,sorts,fields,on: echoed back exactly as stored. These are fixed at creation;status,config, andnotificationsare the mutable parts.
How a watch runs
Create the watch
POST your filters, a config, and one or more notifications channels.
The response returns the whole watch, including its id.Baseline run (free)
Recurring runs
every_hours), the watcher re-runs your filters and
delivers companies that are new since the previous run, up to
max_results_per_run. You are charged 2 credits per company delivered.400 and you narrow it before anything runs. The cap
applies to the baseline alone. Later runs deliver whatever newly matches,
bounded by config.max_results_per_run.What a notification looks like
When a run has matches, wePOST this body to each channel. Discovery records arrive under results.added, each in the company record shape projected to the watch’s fields.
summary.truncated is true when the run matched more companies than max_results_per_run allowed. With the default overflow_policy: "redeliver" the overflow is held back and arrives on a later run rather than being lost.
overflow_policy is accepted on create but is not echoed in the watch
object, so a GET will not show you which policy a watch is on. Record it
on your side if you need to know.Controlling which fields come back
fields picks exactly what each delivered company carries, using the same field-group names as Company Enrich. Omit it and you get the standard projection.
fields does not change the price. A delivered company costs 2 credits
whether it carries one field group or twelve. Every requested field is checked
against your key’s entitlements at creation, so a group your key cannot access
fails the create rather than arriving empty.Realtime watches
A discovery watch reads the Crustdata index. A realtime watch searches the web live on every run instead, so it finds companies as they appear rather than waiting for our index to pick them up. Turn one on withconfig.is_realtime: true. The watch then takes two filter trees
instead of one.
The contract
narrowing_filters then asks the company index about
each company it returned, so it can only drop companies. It never adds any.
Send realtime_filters without the flag and the create fails. Neither can change
afterwards: a PATCH carrying is_realtime, realtime_filters, or
narrowing_filters is refused, so you make a new watch instead.
Filter fields the live company search accepts
These are the only field pathsrealtime_filters takes on a company watch.
Anything else, including any other company search field, belongs in
narrowing_filters.company. A tree with a funding.* field is the exception, as
the funding table below explains.
Firmographics
locations.city and locations.state, so name
the place with locations.headquarters. The watch places each headquarters on a
region the live search knows. A value it cannot place is refused, so pick another value from
autocomplete or use locations.country.
A headquarters already names its country, so an included locations.country
beside it is refused. Put that country in narrowing_filters.company if you need
it. An excluded country (!=, not_in) beside a headquarters works, and
locations.country on its own works too.
Headcount, growth, and revenue
roles.distribution.customer_success_and_support: accounting, administrative,
arts and design, business development, community and social services, consulting,
education, engineering, entrepreneurship, finance, healthcare services, human
resources, information technology, legal, marketing, media and communication,
military and protective services, operations, product management, program and
project management, purchasing, quality assurance, real estate, research, sales,
and customer success and support.
Funding
funding.* field sends the whole realtime_filters tree to the company index,
which answers it as written. That tree takes funding.* fields only. Put location,
size, industry, or any other company condition in narrowing_filters.company. A
tree that mixes them is refused with a 400 that names the first other field.
basic_info.fortune_rank and people.senior_leadership_changed_recently exist only
in the live search, so narrowing_filters.company refuses them too, with
Unknown filter field at narrowing_filters.company: '<field>'. To filter on either
one, leave the funding.* fields out. Without them, both work as realtime filters
beside the other fields in the table above.
No date window is added for you. Without a date the watch follows every company
whose funding matches, so a broad one is refused by the 10,000-company limit. Add
funding.last_fundraise_date to keep it to recent rounds.
You write the same values here as you do on an index watch. Use
Company Autocomplete and
filter values to find them, exactly as you would
for an index-tier watch.
Tree shape
The live search takes an AND of filters, with OR only between values of one filter, sorealtime_filters accepts:
- A top-level
andgroup of leaves, or a single bare leaf. - An
orgroup inside it only when every leaf names the same field and uses an include operator (=,in,(.)). Use it as a value list. - No nested
andgroups. - No
sortson the watch at all. A live search has no stable order.
funding.* field is not bound by these rules. The company index
answers it, so nested groups and an or across different funding fields both work
there.
Exclusion is only where the operator column says so. employee_count_range,
fortune_rank, and followers.count take no negation because the live search
cannot exclude on them; put that negation in narrowing_filters.company, where every negation
operator works.
What goes in narrowing_filters
Everything the live search cannot be asked. A company the live search just found
may be too new for the company index to hold every detail of, so narrowing asks
the company index about the company itself:
realtime_filters: an or
across two different fields, nested groups, and every negation operator all work,
because an index answers it rather than the live search.
company is the only block a company watch takes. Name person, job, or
social_post and the create is refused:
narrowing_filters.company:
- Any company field not in the tables above, such as
basic_info.year_foundedortechnographics.technologies.name. oracross two different fields, nested groups, and every negation operator.
narrowing_filters out and nothing is dropped. Every company the
live search returns is delivered.Send a block and it is checked against the company index. A company the index
does not hold is dropped rather than delivered unchecked, so add a block when
you would rather miss a company than deliver one nobody checked.or spanning both keys. They are separate, which matches
what the watch can do: a company the live search never returned cannot be brought
back by a later stage.
Limits
Realtime examples
Companies growing headcount, narrowed by founding year
Companies growing headcount, narrowed by founding year
basic_info.year_founded has no live filter behind it, so it narrows afterwards
through the company block.headcount.total bounds
land on bucket edges, so they become one size band rather than two filters.Companies staffing up one department
Companies staffing up one department
roles.distribution.* and roles.growth_yoy.* both take the department in the
field path. This watch follows engineering teams over 50 people that grew more
than 20% in a year.filters is optional on a realtime
watch, and comes back null when you leave it out.Companies that just raised
Companies that just raised
realtime_filters tree to the company index, and
that tree takes funding fields only. Location and size go in
narrowing_filters.company. This watch follows San Francisco companies with at
least 11 employees whose last round was seed, Series A, or Series B, raised in the
last 30 days. It is sent with "preview": true, so nothing is created: the response says
whether the watch can be created and shows a sample. Drop the flag to create it.sample trimmed to one company. The funding date is yours to choose. Leave it out
and the watch follows every company whose last round matches, which can exceed
the 10,000 companies a realtime watch may follow.Companies past a size floor that are still growing fast
Companies past a size floor that are still growing fast
headcount.total with only a =>
bound sets a floor and leaves the top open.The first hire into a department
The first hire into a department
engineering for any other
department in the list above.Realtime errors
Every failure is a400 in the usual envelope. error.metadata[].field tells a
realtime_filters problem from a narrowing_filters one, and names the block.
filters on a realtime watch
filters on a realtime watch
A headquarters the watch cannot place
A headquarters the watch cannot place
"preview": true) answers 200 with can_create: false and the same
message in error, so you can check a place before you create the watch:Excluding a size band
Excluding a size band
config.refresh_frequency_days on a discovery watch
config.refresh_frequency_days on a discovery watch
is_realtime, so it has no freshness
cadence of its own. The key is refused on both tiers, whatever value you give it.Runs scheduled more often than daily
Runs scheduled more often than daily
Delivery channels
Every watch takes anotifications array. Add one or more and matches fan out to all of them.
notifications array is valid. The watch runs and records everything, pushes nothing, and you read each run from the run-history endpoints.
Where the records travel
By default a run’s companies arrive inside the notification, underresults. Set config.payload_delivery_type to "link" and the body carries no records at all. It carries metadata.payload_delivery.url instead, a pre-signed S3 URL for one NDJSON file holding every record of that run.
Switch when a run can outgrow what your receiver accepts. An AWS Lambda proxy integration caps a request at 6 MB and an ALB target at 1 MB, and an oversize body is refused whole.
metadata.payload_delivery.type rather than testing for results: a link watch falls back to an inline body if the file cannot be written. See Payload delivery for the file format, the link lifetime, and the errors.
Filters from other datasets
A company watch is not limited to company fields. Drop a job field or a person field into the same flatconditions list and the watch filters on the roles a company is hiring for, or on the people it employs:
What you can combine
The rules in one place
Field names decide the dataset, so there is nothing to declare:headcount.total
is a company field, job_details.title a job field, and
experience.employment_details.current.title a person field. Conditions from
another dataset are answered first, come back as a set of company IDs, and narrow
your watch from there. Delivery, deduplication, per-run caps, and the payload
shape are unchanged, and you are still charged 2 credits per company. Reading the
other dataset is free.
A cross-dataset condition may cover at most 1,000,000 companies, checked at
creation and measured on the cross-dataset conditions alone. Row-level negation
(!=, not_in, (!), not_contains, geo_exclude) is rejected on a condition
from another dataset, because it matches companies with some other matching row
rather than companies with none.
The payload carries companies and nothing else. To see the posting or the person
that put a company in the feed, call POST /job/search or POST /person/search
yourself with the delivered crustdata_company_id.
See Cross-dataset filters for the join
rules, the grouping semantics, the lookup fields for that follow-up call, the
limits with their exact errors, and how timing and deduplication change.
Test your receiver before a run fires
Push one sample notification through a watch to verify your receiver end to end, without waiting for a company to actually appear.metadata.test: true. The call records nothing: it creates no run, does not advance the seen set, and costs no credits.
Manage a watch
search/ segment that create and manage use:
you create at /watch/company/search and read runs at
/watch/company/{id}/runs.Reading a run
runs returns each run newest first, with status, the five record counters below, credits_deducted, and notification_http_status.
A non-2xx notification_http_status means the run found matches and the delivery itself was rejected, so check it before concluding a quiet channel means nothing happened. On a watch with several channels the webhook wins. You get the webhook’s status when the run sent one, and null when that webhook never answered. A run that sent only Slack or email reports 200, and a run that sent nothing reports null.
Every run carries five counters. Each one answers a different question, and two
of the five apply to one watch kind only:
records_searched minus
records_stale is records_after_filter. A large records_stale means the query
is broad and you have already seen most of what it finds.
On an entity run, a gap between records_searched and records_matched is the
part of your list that matches no record Crustdata holds. No other field reports
that gap, and every run skips those identifiers in silence.
new_records_count is below records_after_filter when max_results_per_run
cut the run short. On a realtime discovery watch, records_searched counts the
rows the live retrieval returned.
A first run is where those last two counters sit furthest apart. It delivers a
free sample of up to five records, so new_records_count is at most 5 however
large records_after_filter is. You did not lose the rest. The baseline marked
every one of those records as handled, which is why later runs report only
movement. A first discovery run has nothing to call stale yet, so records_stale
is 0 and records_searched equals records_after_filter.
records_matched and records_stale shipped after the other three. A run
that finished before then carries null for both, whatever its watch kind.Examples
Worked recipes you can copy, paste, and adapt. Each one is a full working request, verified against the live API. Swap the notification channel for your own. For the operator list, field catalog, and validation rules, see the Company Search reference.Find new well-funded companies in a country
Find new well-funded companies in a country
locations.country accepts both ISO 3166-1 alpha-3 codes ("USA", "CAN")
and full country names ("United States", "Canada").Track a segment crossing a growth band
Track a segment crossing a growth band
headcount.growth_percent.6m, and the same shape for
.1m, .3m, .12m) are filterable. headcount.growth_absolute.* gives you
absolute employee deltas over the same periods.Follow high-growth mid-market companies
Follow high-growth mid-market companies
Watch a revenue band
Watch a revenue band
=> and =< for inclusive bounds. Crustdata writes them that way round;
>= and <= return unknown operator.Follow an investor's new portfolio companies
Follow an investor's new portfolio companies
funding.investors to catch every company an investor backs, and pair it with a date so the feed is about recent activity. This watch delivers Sequoia-backed companies whose latest round closed on or after 2025-01-01.funding.investors covers known investors across the company’s whole funding
history. Combined with a funding.last_* filter it means the company has that
investor and its latest round matches, not that the investor joined the latest
round.Catch companies raising a Series A or B
Catch companies raising a Series A or B
funding.last_round_type with funding.last_fundraise_date for a feed of recent rounds at a specific stage.series_a, series_b,
seed. sorts sets the delivery order within a run, which matters when a run
is capped by max_results_per_run.Follow a category, like AI
Follow a category, like AI
taxonomy.categories gives you finer segmentation than industry alone. This watch delivers AI companies with more than 50 employees.Follow newly founded companies
Follow newly founded companies
basic_info.year_founded for a sourcing feed of young companies, checked weekly.Watch a radius around a city
Watch a radius around a city
geo_distance operator on locations.headquarters bounds a watch to companies headquartered near a point. This watch delivers companies with more than 100 employees within 50 km of Palo Alto.value centre is either a location string, geocoded server-side, or an
explicit lat_lng pair, which skips geocoding. distance is required;
unit defaults to km and also accepts mi. See the
geo_distance reference.Use or and nested logic
Use or and nested logic
or group inside an and group when a company should match any of several conditions but must also satisfy a hard requirement. This watch delivers US companies that are either software companies or have raised more than $5M.Cross-dataset: companies hiring for a role
Cross-dataset: companies hiring for a role
POST /job/search with the delivered crustdata_company_id and the same job conditions. See Records from the other dataset.Cross-dataset: companies hiring away from a rival
Cross-dataset: companies hiring away from a rival
Cross-dataset: sales hiring at fast-growing US companies
Cross-dataset: sales hiring at fast-growing US companies
Cross-dataset: three datasets in one watch
Cross-dataset: three datasets in one watch
Cross-dataset: "first time" signals, like a first ML hire
Cross-dataset: "first time" signals, like a first ML hire
Deliver a large run as a linked file
Deliver a large run as a linked file
metadata.payload_delivery.url, a pre-signed URL for one NDJSON file holding the whole run, and no results key. See Payload delivery.Run a watch with no channel and pull the results
Run a watch with no channel and pull the results
notifications: [] and the watch pushes nothing. It still runs on schedule, still records every match, and you read each run yourself.Error handling
A company discovery watch answers every failure the same way, whether you are creating a watch, editing one, testing delivery, or reading run history.type is the machine-readable code you branch on, message is the sentence you
can show a user, and metadata carries per-problem detail when there is any.
Read error.message, not the top level: there is no detail key and no
non_field_errors key on any watcher response.
Every problem in one response
Validation collects every failure in one pass, so a payload with three things wrong costs one round trip rather than three.metadata carries one
entry per problem, and message is those entries joined in order:
Exact responses
Authorization header and a wrong key give the same 401 body, so
the response does not tell you which of the two happened.
POST /watch/widget/search returns the gateway’s
{"error_msg": "404 Route Not Found"}, and an entity path on a dataset that
has no entity watcher returns an HTML 404. Branch on the status code first,
then parse.Pricing
What to do next
- Watch a known list instead: use the Company Entity Watcher to track profile changes on companies you already hold.
- Look up operators and fields: see the Company Search reference for the full filter catalog.
- Check a filter before you watch it: run it through Company Search and confirm
total_countis not zero. - Choose how records travel: see Payload delivery for inline versus linked NDJSON.

