DocsMonitor typesSERP tracking

SERP tracking

SERP monitors watch a search engine's results page for a keyword. They're a separate monitor type from structured (extraction) monitors and visual monitors: no URL, no selectors - just a keyword, an engine and a market. Verid renders the search directly in a real browser (a paid provider only steps in as a fallback, and only for google, when the direct check comes back blocked), and every check is stored so the dashboard can chart it over time at /serp.

The /v1/serp-monitors API has two modes, set with the mode field:

  • rank_tracking (default) - track one domain's position for a keyword. Every run stores target_rank and rank_delta, and a delivery fires once the rank moves past a threshold or the domain enters/leaves the results.
  • full_analysis - no target domain. Give it a keyword and get the entire result list for the pages you chose back as JSON on every check: rank, title, description and domain (the site's origin, like https://www.g2.com/) for every result. A delivery fires when anything in that list changes between checks, and the dashboard shows the current list diffed against the previous one.

Both modes run the identical search and extraction - full_analysis just skips the target-domain lookup that rank_tracking does on top of the same data.

Creating one

Dashboard - go to SERP tracking → New SERP monitor. Two buttons at the top pick the mode; the fields below them change to match, and a live example shows what the request and response look like before you submit.

API - rank_tracking:

POST /v1/serp-monitors
curl -X POST https://api.verid.dev/v1/serp-monitors \
  -H "Authorization: Bearer $VERID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "best invoicing software - us",
    "keyword": "best invoicing software",
    "mode": "rank_tracking",
    "engine": "google",
    "target_domain": "yoursite.com",
    "location_code": "us",
    "language_code": "en",
    "schedule_interval_seconds": 3600,
    "rank_change_threshold": 3,
    "deliveries": [
      { "type": "webhook", "url": "https://your-app.com/hooks/rank-shift" }
    ]
  }'

full_analysis - no target_domain (the API rejects it in this mode) and no rank_change_threshold (ignored):

POST /v1/serp-monitors
curl -X POST https://api.verid.dev/v1/serp-monitors \
  -H "Authorization: Bearer $VERID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SERP analysis - best invoicing software",
    "keyword": "best invoicing software",
    "mode": "full_analysis",
    "engine": "google",
    "pages": 3,
    "location_code": "us",
    "language_code": "en",
    "schedule_interval_seconds": 21600,
    "deliveries": [
      { "type": "webhook", "url": "https://your-app.com/hooks/serp-analysis" }
    ]
  }'

engine defaults to google and also accepts bing, yahoo, duckduckgo and yandex. Neither mode has keyword-volume or keyword-difficulty data and nothing is backfilled - pair it with your research tool for those.

Configuration reference

FieldTypeDefaultMeaning
keywordstring-The search query to check.
moderank_tracking | full_analysisrank_trackingTrack one domain's rank, or return the full result list for the pages you chose.
enginestringgooglegoogle, bing, yahoo, duckduckgo or yandex.
target_domainstring-Domain or URL fragment to find in the results and track the position of. rank_tracking only - sending it with mode full_analysis returns a 422, and switching a monitor to full_analysis clears it.
location_codestringusTwo-letter country code.
language_codestringenTwo-letter language code.
devicedesktop | mobiledesktopWhich layout to render.
pagesnumber (1-5)1How many pages of the engine's results to check. A page is usually about 10 results (Yahoo shows 7), so 5 covers roughly the top 50. Every check uses one run per page and takes longer as you add pages.
schedule_interval_secondsnumber-Check frequency in seconds. The fastest allowed is 600 (every 10 minutes) on every plan.
rank_change_thresholdnumber-Fire a delivery once target_rank moves by this many positions, or the domain enters/leaves the results. rank_tracking only - ignored in full_analysis.
deliveriesarray[]Webhook, Slack, Discord or email destinations.

How a check works

  1. Search - the keyword is rendered on the chosen engine in a real browser first. Only google falls back to a paid provider (DataForSEO or Serper.dev), and only when the direct check comes back blocked. With pages above 1, the later pages open in the same browser session a few seconds apart, through the engine's own next page link, so a 5 page check can take up to a minute longer than a 1 page one. The paid fallback asks for each page separately.
  2. Extract - every organic result on the pages you asked for is parsed out: position, title, URL, domain and the description snippet under the title. Positions run on across pages (if page 1 had 10 results, the first result on page 2 is 11), and a URL the engine repeats on a later page is counted once. This is the same list regardless of mode.
  3. Match or compare - in rank_tracking, target_domain is looked up inside that list to get target_rank; in full_analysis this step is skipped entirely and target_rank/rank_delta stay null.
  4. Decide - rank_tracking fires a delivery when rank_change_threshold is crossed or the domain enters/leaves the results. full_analysis fires only when the result list differs from the previous check - any change to a result's rank, title, description or domain counts. A check that comes back identical to the last one never triggers a notification, so it's safe to run on a tight schedule.

What alerts contain

A rank_tracking delivery carries the rank movement in a serp block:

json
{
  "id": "d_2h8Kq...",
  "version": "2026-05-01",
  "monitor_id": "9f3c...",
  "run_id": "r_71ba...",
  "fired_at": "2026-08-11T09:14:02.418Z",
  "diff": { "fields_changed": [], "before": {}, "after": {} },
  "serp": {
    "keyword": "best invoicing software",
    "target_domain": "yoursite.com",
    "rank": 7,
    "previous_rank": 3,
    "rank_delta": 4
  },
  "monitor": { "name": "best invoicing software - us", "url": "https://www.google.com/search?q=..." }
}

A full_analysis delivery carries the full result list in a serp_analysis block instead, with the previous check's list in previous_results so you can diff the two yourself:

json
{
  "id": "d_2h8Kq...",
  "version": "2026-05-01",
  "monitor_id": "9f3c...",
  "run_id": "r_71ba...",
  "fired_at": "2026-08-11T09:14:02.418Z",
  "diff": { "fields_changed": [], "before": {}, "after": {} },
  "serp_analysis": {
    "keyword": "best invoicing software",
    "results": [
      { "rank": 1, "title": "Best invoicing software of 2026", "description": "Compare the top invoicing tools side by side.", "domain": "https://www.capterra.com/" },
      { "rank": 2, "title": "Top-rated invoicing software - G2", "description": "Real user reviews for every invoicing platform.", "domain": "https://www.g2.com/" }
    ],
    "previous_results": [
      { "rank": 1, "title": "Top-rated invoicing software - G2", "description": "Real user reviews for every invoicing platform.", "domain": "https://www.g2.com/" },
      { "rank": 2, "title": "Best invoicing software of 2026", "description": "Compare the top invoicing tools side by side.", "domain": "https://www.capterra.com/" }
    ]
  },
  "monitor": { "name": "SERP analysis - best invoicing software", "url": "https://www.google.com/search?q=..." }
}

diff is always empty on a SERP delivery, in either mode - there are no user-named fields, so the actual change lives in the serp/serp_analysis block instead. Every run's history (GET /v1/serp-monitors/{id}/runs) also carries the full organic_results array regardless of mode - target_rank/rank_delta are just a lookup inside it, so it's always there to inspect even on a rank_tracking monitor. Each item has position, title, url, domain and description (the search snippet, or null when the engine's markup didn't expose one for that result). Deliveries are HMAC-SHA256 signed the same way as every other monitor type; fetch the plaintext secret once from GET /v1/serp-monitors/{id}/signing-secret.

Retention and limits

  • SERP tracking ships on its own "+ SERP" plans (Lite/Starter/Pro/Scale + SERP) - it's not included in the base paid plans. The 14-day free trial includes SERP tracking from signup, with 100 SERP runs for the whole trial.
  • There's no cap on the number of SERP monitors. Checks can run as often as every 10 minutes on any plan, the same in both modes.
  • Each check uses one run per page in pages from your plan's monthly runs: a 1 page monitor uses 1 run per check, a 5 page monitor uses 5. More pages also make each check slower. If a check needs more runs than you have left this period, it's skipped until the runs reset at the next billing period.
  • If a later page can't be loaded (the engine blocks it), the check keeps the pages it did get and still uses the full pages count of runs. Alerts only compare the results both checks covered, so a shorter check, or changing pages, never fires an alert on its own.
  • A monitor auto-pauses after 10 consecutive failed checks, and you get an email when that happens.

When to use which

You want to know...Use
Where does my site rank for this keyword?mode: "rank_tracking" with target_domain set
What's on page 1 for this keyword right now, and when does it change?mode: "full_analysis"
Did an AI Overview, featured snippet, or People Also Ask box appear?rank_tracking's ai_overview_present/featured_snippet_present booleans, or the advanced method below if you need the actual text
What does a competitor's listing title/description say, verbatim?The advanced method below - full_analysis gives you title/description per result already, but the advanced method lets you extract anything else on the page too

Advanced: hand-built selectors

For anything neither mode covers - the AI Overview or featured snippet's actual text, People Also Ask questions, or any other on-page element - build a regular monitor against the search results URL with extract_config:

POST /v1/monitors
curl -X POST https://api.verid.dev/v1/monitors \
  -H "Authorization: Bearer $VERID_API_KEY" \
  -d '{
    "name": "SERP - best invoicing software",
    "url": "https://www.google.com/search?q=best+invoicing+software&hl=en&gl=us&pws=0",
    "fetch_mode": "browser",
    "schedule_interval_seconds": 1800,
    "extract_config": {
      "method": "xpath",
      "fields": {
        "top_result_title":  "(//div[@id=\"search\"]//h3)[1]",
        "top_result_url":    "(//div[@id=\"search\"]//a[h3])[1]/@href",
        "ai_overview":       "//div[contains(@aria-label,\"AI Overview\")]",
        "featured_snippet":  "//div[@data-attrid=\"wa:/description\"]"
      }
    },
    "diff_predicate": { "type": "any_field_changes" },
    "deliveries": [
      { "type": "webhook", "url": "https://your-app.com/hooks/serp-shift" }
    ]
  }'

Notes

  • Google's results are JavaScript-rendered, so a plain fetch can never see them. Verid detects search-results URLs and renders them in a browser even on fetch_mode: "auto"; setting "browser" explicitly is still the clearest way to express the intent.
  • Pin hl, gl, and pws=0 in the URL so personalization can't introduce noise.
  • Google removed &num=100 in September 2025. A results page now holds 10 entries, so tracking deeper positions means separate monitors on &start=10, &start=20, and so on.
  • Avoid &udm=14: it renders, but result links come back wrapped in google.com/goto?url=…, so any @href field extracts an opaque blob instead of the destination.
  • When Google answers with its "unusual traffic" page instead of results, Verid records the check as blocked. It never stores the block page, so a blocked check can't fire a false change alert.
  • A refusal is not retried, because retrying it does not work: in testing, once Google had flagged an IP, 16 of 16 further attempts were refused over 70 minutes regardless of how the request was made. Instead Verid stops querying that engine for 15 minutes, doubling per consecutive refusal up to 6 hours, and checks during that window fail immediately with the reason. The first healthy check clears it.
  • Google is the hardest target here by a wide margin. If you need results you can depend on rather than best-effort, monitor Bing, DuckDuckGo or Yahoo - none of them gate the results document on JavaScript, and none of them refuse the way Google does.
  • Selectors evolve when Google ships layout changes; expect to update them occasionally.
  • For high-frequency or geo-specific tracking, configure the residential proxy layer so layer-3 fallback can route requests from the right region.

For ranking-shift strategy, proxy/geo notes, and the full payload, see the SERP Monitoring use case.