Looking for the overview — what this API returns, what it costs, and a call you can run without a key? See the Change-Detect API page →
Developer Tools

Change-Detect API & Scraper

The Change-Detect API returns website content-monitoring and change-detection data as clean JSON.

5 actionsLive JSON1,000 free credits$0.67–$1.50 / 1,000 creditsMCP-ready
Get a free keyOpen in playground

🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.

The primary snapshot endpoint returns a page's content hash, structural hash, text hash, title, meta description, main content, normalized text, word count and links — a fingerprint you can compare over time. You can also diff two snapshots, run a monitor_check, extract structured content and batch multiple URLs. It is built for change monitoring, compliance and competitive-intelligence workflows that need to detect when a page changes without building a diffing pipeline. One ReefAPI key, one shared credit pool, the standard envelope.

Reference

Which value to store, and what each mode compares

This engine keeps nothing between calls. You store the fingerprint and hand it back, so picking the right field to store is the whole design decision. The hashes and counts below were measured against example.com and iana.org on 2026-08-27.

Field or parameterBehaviorMeasured
data.content_hash64 hex characters. This is the value to store and pass back as baseline_hashexample.com = feb057ddba5ac313506909af05c17370808a81030f057346499d045c25534dbf
meta.content_hashThe first 16 characters only, a short echo for logs. Do not store this onefeb057ddba5ac313
text_hashIdentical to content_hash when no selector is setBoth fields matched character for character on example.com
structural_hashDOM tag skeleton. Holds still when only wording movesAcross two entirely different pages, structural_changed stayed false while the text diff read 100%
baseline as an object or raw textYou get added[], removed[], modified[] and change_ratio13 blocks removed, 3 modified, change_ratio 1.0
baseline as a bare 64-hex hashchanged is still answered, but the block lists come back empty and change_ratio nullsummary says so verbatim: textual diff unavailable, baseline was a hash, not full content
selectorFull CSS or XPath, both accepted in the same parameter'h1' and '//h1' on the same page produced the identical hash 8d7e39b434a6bd30... and word_count 2
selector with no matchFalls back to the whole page and says so, it does not errorselector_matched false, word_count back at the full-page value, ok:true
mode=linksCompares the outbound link set only. change_ratio is null in this modeadded_count 1, removed_count 42 between example.com and an IANA page
batchUp to 20 URLs, each guarded independently, partial failure allowed3 URLs, ok_count 2, the third an inline NOT_FOUND for a host that does not resolve

Private, internal and cloud-metadata targets are refused. A snapshot of http://169.254.169.254/latest/meta-data/ returned INVALID_PARAM with detail.ssrf_blocked true and the resolved address echoed back. Every redirect hop is validated the same way, so a public host that redirects inward is stopped mid-chain.

Live example

Real request and response JSON

Captured from the indexed primary action, snapshot, on .

Captured request
{
  "method": "POST",
  "url": "https://api.reefapi.com/change-detect/v1/snapshot",
  "headers": {
    "x-api-key": "$REEF_KEY",
    "content-type": "application/json"
  },
  "body": {
    "url": "https://example.com"
  }
}
Captured response
{
  "ok": true,
  "meta": {
    "api": "change-detect",
    "endpoint": "snapshot",
    "mode": "live",
    "latency_ms": 679.4,
    "record_count": 1,
    "bytes": 559,
    "cache_hit": false,
    "method": "fetch_normalize_fingerprint_bare_dc_curl_cffi",
    "ssrf_guarded": true,
    "noise_resistant": true,
    "final_url": "https://example.com",
    "content_hash": "feb057ddba5ac313",
    "likely_js_app": false
  },
  "data": {
    "content_hash": "feb057ddba5ac[redacted-phone]af05c17370808a81030f[redacted-phone]d045c25534dbf",
    "structural_hash": "512c91013abc5671902f6fa2f0575d0f64927e8bd61885798eb04655a048b68f",
    "text_hash": "feb057ddba5ac[redacted-phone]af05c17370808a81030f[redacted-phone]d045c25534dbf",
    "title": "Example Domain",
    "meta_description": null,
    "main_content": "Example Domain\nThis domain is for use in documentation examples without needing permission. Avoid use in operations.\nLearn more",
    "normalized_text": "Example Domain\nThis domain is for use in documentation examples without needing permission. Avoid use in operations.\nLearn more",
    "word_count": 19,
    "links": [
      "https://iana.org/domains/example"
    ],
    "link_count": 1,
    "content_type": "text/html",
    "status": 206,
    "fetched_at": "[redacted-phone]T17:19:28Z",
    "byte_size": 559,
    "likely_js_app": false,
    "final_url": "https://example.com",
    "selector": null,
    "selector_matched": null
  }
}
Actions

What the Change-Detect API does

ActionDescriptionConcrete use caseKey params
snapshotFetch a URL and return a noise-resistant fingerprint: content_hash (normalised main content, rotating-token-stripped), structural_hash (DOM skeleton), text_hash, title, meta_description, main_content (boilerplate-stripped), word_count, links[]. Store the returned snapshot/hash and feed it back to diff/monitor_check later.Platform and DevOps teams call snapshot to fetch a URL and.url, selector, follow_redirects
diffDiff a page against a prior baseline (snapshot object OR content_hash OR raw text), or compare two live URLs (url + url2). Returns changed(bool), change_ratio(0..1), change_type(text|structural|both|none), added[]/removed[]/modified[] blocks, and a human summary — all on the NOISE-RESISTANT normalised content.Security and supply-chain teams call diff to get diff a page against a prior baseline (snapshot object OR content_hash OR raw text), or compar….url, baseline, url2, selector, mode, ...
monitor_checkThe stateless half of monitoring: re-fetch the URL and tell you FAST whether it changed since a known hash. Pass baseline_hash (+ optional selector/mode). Returns changed(bool) + current_hash + a short diff_summary — the cheap 'did it change?' path.Developer-tool builders call monitor_check to get the stateless half of monitoring.url, baseline_hash, selector, mode, follow_redirects
extractMain-content extraction only — boilerplate (nav/header/footer/ads/cookie/comments) stripped → clean text + structured blocks + links. Useful to feed clean diffs or to get just the article. No fingerprint comparison.AI-agent developers call extract to get main-content extraction only.url, selector, follow_redirects
batchSnapshot up to 20 URLs concurrently in one call (each independently SSRF-guarded, shared concurrency). Returns a compact fingerprint per URL.Platform and DevOps teams call batch to get snapshot up to 20 URLs concurrently in one call (each independently SSRF-guarded, shared conc….urls, selector
Code samples

Call snapshot from your stack

curl -X POST https://api.reefapi.com/change-detect/v1/snapshot \
  -H "x-api-key: $REEF_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"https://example.com"}'
MCP one-liner
Ask your MCP-connected assistant: call reefapi.change-detect.snapshot with {"url":"https://example.com"}.
Use cases

Who uses this API and why

  • Competitive-intelligence tools call snapshot then diff to detect when a competitor changes pricing or copy.
  • Compliance teams use monitor_check to alert when a terms or policy page is updated.
  • Content pipelines use extract and batch to normalize and track many pages at once.
FAQ

Questions developers ask before integrating

Where is my baseline stored?

Nowhere. Every action is a single fetch: you get a fingerprint back, you store it, and you pass it in next time. That is the trade. There is no watch to create, no schedule to pay for and no per-monitor cap, and equally no history you can ask us for later. Scheduling belongs in your cron or queue; this engine is the compare step.

Can I feed a hash straight from snapshot back into monitor_check?

Yes, and it round-trips cleanly. Taking content_hash feb057dd... from a snapshot of example.com and passing it as baseline_hash to monitor_check returned changed:false, hash_kind 'content_hash' and diff_summary 'Unchanged since baseline (hash matches).' The engine works out for itself whether the 64-hex value you sent is a content_hash or a text_hash and reports which one it matched in hash_kind, so you do not have to track that separately.

Why are added[] and removed[] empty when I pass a hash?

Because a hash carries no text to diff against. baseline_kind tells you which path you took. 'hash' gives you changed, current_hash and a summary, with change_ratio null and change_type 'unknown'. 'url2', a full snapshot object or raw prior text gives you the block lists. If you want the wording that moved, store normalized_text or the whole snapshot object next to the hash and pass the object as baseline.

What does selector_matched: false mean, and is it an error?

It means your CSS selector or XPath matched nothing, so the whole page was fingerprinted instead, and the call still returns ok:true. That is the dangerous case for a price watch: your selector silently widened to the entire page, and from then on every unrelated edit reads as a change. Assert on selector_matched being true in your own code before trusting a scoped hash.

Does the selector accept XPath as well as CSS?

Both, in the same parameter, told apart by shape. On the IANA example-domains page, selector 'h1' and selector '//h1' produced the identical content_hash and the identical word_count of 2. Use whichever expresses the region better. XPath is usually the shorter route to a specific table cell, for example //table//tr[2]/td[3].

status came back 206 instead of 200. Is my content truncated?

Not necessarily. status is the upstream HTTP status reported verbatim, and example.com answered 206 while returning its complete 559-byte body, the correct title, 19 words and its single outbound link. Treat any 2xx as a good fetch and judge completeness from byte_size and word_count rather than from the status code alone.

What makes the diff noise-resistant?

Comparison runs on extracted main content rather than raw HTML: navigation, header, footer, ads, cookie banners and comment blocks are stripped, and rotating tokens are normalized away before hashing. The practical proof is that the same page fetched twice produced the identical content_hash despite a new fetched_at each time. If you want the cleaned text without any comparison, `extract` returns it with a blocks[] array (16 blocks, 120 words on the IANA page) and no fingerprint work.

What happens to one bad URL in a batch of twenty?

It fails on its own and the rest still come back. A three-URL batch containing a hostname that does not resolve returned ok:true at the top level with count 3 and ok_count 2, while the failing row carried an inline error object with code NOT_FOUND and the message that the host does not resolve. Read ok_count, not the top-level ok, whenever you batch.

What is the Change-Detect API?

Change-Detect API is a ReefAPI endpoint group for change-detect It returns live JSON through POST requests under /change-detect/v1.

Is the Change-Detect API free to try?

Yes. ReefAPI starts with 1,000 free credits, no card required. Change-Detect calls use the same shared credit balance as every other ReefAPI engine.

Do I need a Change-Detect login or account?

No login to Change-Detect is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.

How fresh is the Change-Detect data?

The page example is captured from a live snapshot call, and production requests fetch live data through ReefAPI rather than a static sample.

How many credits does the Change-Detect API use?

Change-Detect actions currently cost 1 credit per successful call. Failed or blocked calls are free, and all APIs draw from one credit pool.

Can I call Change-Detect from an AI assistant or MCP client?

Yes. Connect ReefAPI once through MCP and your assistant can call change-detect actions with the same key, credit pool and JSON envelope used by normal REST requests.

docs / change-detect

Change-Detect

Change-Detect

base /change-detect/v15 endpoints
post/change-detect/v1/snapshot1 credit

Fetch a URL and return a noise-resistant fingerprint: content_hash (normalised main content, rotating-token-stripped), structural_hash (DOM skeleton), text_hash, title, meta_description, main_content (boilerplate-stripped), word_count, links[]. Store the returned snapshot/hash and feed it back to diff/monitor_check later.

ParameterAllowed / rangeDescription
urlrequiredThe page to fingerprint. Full URL (https://example.com/page) or bare domain (example.com → https:// assumed). Only http/https; private / internal / cloud-metadata targets are SSRF-blocked. Alias: target.
selectoroptionalScope detection to a region instead of the whole page. Accepts a FULL CSS selector ('div.price-box > span.amount', '.product:nth-child(2)', '[data-testid=total]') OR an XPath ('//div[@id="price"]', '//table//tr[2]/td[3]') — lxml-backed. If the selector matches nothing the whole page is used and `selector_matched:false` is reported.
follow_redirects = trueoptionalFollow redirects (http→https, apex→www) before fingerprinting the final URL (default true). Each hop is independently SSRF-validated.
Try in playground →
post/change-detect/v1/diff1 credit

Diff a page against a prior baseline (snapshot object OR content_hash OR raw text), or compare two live URLs (url + url2). Returns changed(bool), change_ratio(0..1), change_type(text|structural|both|none), added[]/removed[]/modified[] blocks, and a human summary — all on the NOISE-RESISTANT normalised content.

ParameterAllowed / rangeDescription
urlrequiredThe page to fingerprint. Full URL (https://example.com/page) or bare domain (example.com → https:// assumed). Only http/https; private / internal / cloud-metadata targets are SSRF-blocked. Alias: target.
baselineoptionalThe prior state to diff against: a snapshot object returned by a previous `snapshot` call, OR a 64-hex content_hash, OR raw prior text/HTML. (diff needs `baseline` OR `url2`.)
url2optionalSecond URL — when comparing two live pages (A vs B) instead of a page against a stored baseline. SSRF-guarded like `url`.
selectoroptionalScope detection to a region instead of the whole page. Accepts a FULL CSS selector ('div.price-box > span.amount', '.product:nth-child(2)', '[data-testid=total]') OR an XPath ('//div[@id="price"]', '//table//tr[2]/td[3]') — lxml-backed. If the selector matches nothing the whole page is used and `selector_matched:false` is reported.
mode = textoptionaltext · structural · full · linksWhat to compare: text (main content, default) | structural (DOM skeleton) | full (both) | links (outbound link set).
follow_redirects = trueoptionalFollow redirects (http→https, apex→www) before fingerprinting the final URL (default true). Each hop is independently SSRF-validated.
Try in playground →
post/change-detect/v1/monitor_check1 credit

The stateless half of monitoring: re-fetch the URL and tell you FAST whether it changed since a known hash. Pass baseline_hash (+ optional selector/mode). Returns changed(bool) + current_hash + a short diff_summary — the cheap 'did it change?' path.

ParameterAllowed / rangeDescription
urlrequiredThe page to fingerprint. Full URL (https://example.com/page) or bare domain (example.com → https:// assumed). Only http/https; private / internal / cloud-metadata targets are SSRF-blocked. Alias: target.
baseline_hashrequiredThe content_hash (or text_hash) from a prior snapshot. monitor_check re-fetches the URL and tells you, fast, whether it changed since this hash. 64-hex.
selectoroptionalScope detection to a region instead of the whole page. Accepts a FULL CSS selector ('div.price-box > span.amount', '.product:nth-child(2)', '[data-testid=total]') OR an XPath ('//div[@id="price"]', '//table//tr[2]/td[3]') — lxml-backed. If the selector matches nothing the whole page is used and `selector_matched:false` is reported.
mode = textoptionaltext · structural · full · linksWhat to compare: text (main content, default) | structural (DOM skeleton) | full (both) | links (outbound link set).
follow_redirects = trueoptionalFollow redirects (http→https, apex→www) before fingerprinting the final URL (default true). Each hop is independently SSRF-validated.
Try in playground →
post/change-detect/v1/extract1 credit

Main-content extraction only — boilerplate (nav/header/footer/ads/cookie/comments) stripped → clean text + structured blocks + links. Useful to feed clean diffs or to get just the article. No fingerprint comparison.

ParameterAllowed / rangeDescription
urlrequiredThe page to fingerprint. Full URL (https://example.com/page) or bare domain (example.com → https:// assumed). Only http/https; private / internal / cloud-metadata targets are SSRF-blocked. Alias: target.
selectoroptionalScope detection to a region instead of the whole page. Accepts a FULL CSS selector ('div.price-box > span.amount', '.product:nth-child(2)', '[data-testid=total]') OR an XPath ('//div[@id="price"]', '//table//tr[2]/td[3]') — lxml-backed. If the selector matches nothing the whole page is used and `selector_matched:false` is reported.
follow_redirects = trueoptionalFollow redirects (http→https, apex→www) before fingerprinting the final URL (default true). Each hop is independently SSRF-validated.
Try in playground →
post/change-detect/v1/batch1 credit

Snapshot up to 20 URLs concurrently in one call (each independently SSRF-guarded, shared concurrency). Returns a compact fingerprint per URL.

ParameterAllowed / rangeDescription
urlsrequiredList of URLs to snapshot in one call (max 20); each is independently SSRF-guarded and fetched concurrently.
selectoroptionalScope detection to a region instead of the whole page. Accepts a FULL CSS selector ('div.price-box > span.amount', '.product:nth-child(2)', '[data-testid=total]') OR an XPath ('//div[@id="price"]', '//table//tr[2]/td[3]') — lxml-backed. If the selector matches nothing the whole page is used and `selector_matched:false` is reported.
Try in playground →