YOUR_API_KEY in each example with your actual API key. All requests
require the x-api-version: 2025-11-01 header. This endpoint is open to all
API customers, with no per-endpoint grant needed.Request body
config
every_hours frequency only makes changes surface sooner. It does not raise
the price.Response body
Create returns the whole watch, so you can confirm what was stored before the baseline run starts.Rate limits and credits
changes array. After that you pay
5 credits per notification, one charge for each watched
company that changed on a run. A run that surfaces nothing costs nothing, and
the price is the same whether you check hourly or monthly. If your balance is
too low, the run is suspended rather than partially delivered.
For your own price per company, open the
watch builder in the dashboard.- Rate limit: 10 requests per minute on
/watch/company. Each watch path keeps its own budget. See Rate limits. - Watched list: up to 10,000 companies per watch, editable at any time.
- Fixed at creation:
trackandfields. To change what you watch for, create a new watch.
Examples
What you can track
Company field reference
field path.Your first watch: two companies, two signals
Watch two companies and get a message whenever either changes headcount or appears in the news.Understanding the response
idis the handle for everything else: pause, edit the list, cancel, and read run history all take it.fields: nullmeans the default minimal projection. The deliveredrecordwill carry onlybasic_infountil you request more.last_run_at: nulluntil the baseline run completes.config.refresh_frequency_days: 30is echoed on every company entity watch. Company watches run at the 30-day baseline, so this is the only rate that applies to them: 5 credits per changed company.
How a watch runs
Create the watch
POST your entities, a track, a config, and one or more notifications
channels. The response returns the whole watch, including its id.Baseline run (free)
changes array and the
record projected to your fields. Use it to confirm your receiver parses
the envelope before a real change arrives. To read a company’s current profile
on demand, use Company Enrich.Recurring runs
every_hours), the watcher re-checks each company, diffs it
against its last snapshot, and delivers a notification for every company whose
track condition just became true. You are charged 5 credits per
notification.changes: [], so a receiver that keys
on a non-empty changes array skips the whole baseline on its own.What a notification looks like
Every fired company carries two things: thechanges array, which is exactly what moved and is reported whatever fields says, and a record, the company’s current data projected to the fields you requested. The company’s identity travels inside the record as crustdata_company_id.
The example below is from a watch created with fields: ["basic_info", "headcount", "news"].
results is a flat list, one object per fired company, each with its own changes and record. A changed scalar reports as { "field": …, "type": "changed", "from": …, "to": … }; an added array reports as { "field": …, "type": "added", "new_elements": [ … ] }.
To re-read a past run, or audit exactly what was delivered, the run-history endpoint returns the same content under a different envelope:
What you can track
Thetrack describes the change that triggers a notification. It is a tree of condition leaves, optionally combined with and / or groups.
A leaf has the shape:
=> and “less-than-or-equal”
as =<, not >= / <=. Those reversed forms return unknown operator.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 its conditions hold and at least one just became true. Groups nest.
Common company signals
These are examples. A company profile has 500+ addressable fields, and you can track any of them: any scalar withchanged, any array with added.
field paths address the same profile structure the Enrich API returns, so you
can browse every trackable field in the
Company Enrich reference. changed
requires a scalar path and added requires an array path. Mixing them
returns a 400: "changed" on the array news is rejected, and you want
"added" there.track and fields are different
These are two independent knobs, and the distinction matters:
fields does not affect pricing. An entity watch is a flat 5 credits per notification no matter how many field groups you deliver, so there is no cost reason to keep the payload thin. There is no wildcard: to receive the complete record, list every field group your key is entitled to. The delivered record uses the same schema as Company Enrich, with identical field-group names and nesting.
Choosing the companies
entities is an object mapping an identifier type to a list of values. You can mix identifier types in one watch. A single watch holds up to 10,000 companies.
Delivery channels
Add one or more channels tonotifications and every change fans out to all of them.
Where the records travel
By default a run’s change events 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.
Edit the watched list any time
Unlike a saved search, an Entity Watcher’s list is mutable. Add or remove companies as your account book or portfolio changes, without recreating the watch.PATCH the watch with a new entities object:
PATCH status (active / paused), config, and notifications.
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 company to actually move. The test delivers the exact envelope a real run sends, same shape and same signature headers, flagged withmetadata.test: true. It runs no diff, records no snapshot, costs no credits, and persists nothing.
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.
record carries placeholder values with the exact shape of
a real notification, so your parser, signature check, and routing see
production structure without real company data. The changes array mirrors
your watch’s track: a changed scalar reports
{ "type": "changed", "value": …, "from": …, "to": … } (value is the
threshold target for >/< operators, null for a plain changed), and an
added array reports { "type": "added", "new_elements": [ … ] }."count": 3 to receive three sample companies in one envelope, with distinct crustdata_company_ids (814313, 814314, 814315) and summary.delivered: 3, so you can exercise how your receiver iterates results.
<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.Manage your watches
List your watches: GET /watch/company
List your watches: GET /watch/company
status, page with limit and offset.Get one watch: GET /watch/company/{id}
Get one watch: GET /watch/company/{id}
last_run_at is null until the baseline run completes, then carries the timestamp of the most recent run.Pause or resume: PATCH status
Pause or resume: PATCH status
{ "status": "active" } to resume.Delete a watch: DELETE /watch/company/{id}
Delete a watch: DELETE /watch/company/{id}
204 No Content. Deletion is terminal: the watch cannot be resumed.Read a run's counters: GET /watch/company/{id}/runs
Read a run's counters: GET /watch/company/{id}/runs
records_searched: 2
is the list you sent, records_matched: 1 is the part of it that resolved, and
credits_deducted: 0 is the baseline being free. Read the counters to catch that
second domain: no other field reports an identifier that matched nothing.Reading a run
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 every trackablefield path, see the Company Enrich reference.
Alert when an account crosses a headcount threshold
Alert when an account crosses a headcount threshold
headcount in fields so the payload carries the new value in
record, not just the from and to in changes.Follow every funding move on a portfolio
Follow every funding move on a portfolio
or so any one of them fires: a new round type, a new investor, or a new acquisition.funding.investors and funding.acquisitions are arrays, so they take added. funding.last_round_type is a scalar, so it takes changed. Mixing those up returns 400.Watch a competitor's news mentions
Watch a competitor's news mentions
news is an array, so added fires whenever a new item lands. A six-hour cadence surfaces it the same day without costing more, because you pay per notification and not per run.changes entry for an array reads { "field": "news", "type": "added", "new_elements": [ … ] }, so you get the new items themselves without diffing the payload yourself.Catch a rebrand, a relocation, or an industry shift
Catch a rebrand, a relocation, or an industry shift
changed scalars under one or. Useful on a CRM sync: these are the fields that quietly go stale and break matching.Require two things at once with and
Require two things at once with and
and group fires only when every condition holds and at least one just became true. This watch fires when a company is at 1,000 or more employees and its open-role count moves, which reads as “scaling and still hiring”.Track audience growth
Track audience growth
followers.count is a scalar, so changed fires on any movement. Pair it with a weekly cadence unless you want every small fluctuation.Watch companies by profile URL
Watch companies by profile URL
Deliver the full company record
Deliver the full company record
fields set.technographics among them, are
left out here. Adding one your key cannot access fails the create with 403
and the message Not entitled to fields: <group>. Contact support to request access.Deliver a large run as a linked file
Deliver a large run as a linked file
{ changes, record } object, the same shape the inline body carries. See Payload delivery.Error handling
A company entity watch answers every failure the same way, whether you are creating a watch, editing the watched list, 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
fields does not change the price, and neither does every_hours. A run where nothing changed costs nothing. See Pricing for the full catalog.
What to do next
- Find companies instead of tracking them: use the Company Discovery Watcher to deliver companies that newly match a filter.
- Browse trackable fields: see the Company Enrich reference for every
fieldpath. - Read a profile on demand: use Company Enrich when you want current state rather than change.
- Choose how records travel: see Payload delivery for inline versus linked NDJSON.

