RankParse

Rank Tracking

Read stored Google, Bing and Yandex keyword ranking snapshots, history and rank-change alerts for your connected websites.

GET/v1/rank-tracking/rankingsFreeAPI key

The rank-tracking endpoints return stored keyword ranking snapshots collected from the webmaster providers you have connected (Google Search Console, Bing Webmaster Tools, Yandex Webmaster). Snapshots are synced on a schedule, so this is never a live SERP check. All reads cost 0 credits, and alert settings can also be updated with an API key.

Google, Bing and Yandex report different search populations, so RankParse keeps their positions, dates and availability separate. Do not average positions across engines. Compare per engine.

Before you call

Set up tracking in the dashboard

Connect a provider, attach a property to a website and add queries. The walkthrough is in Sites and rank tracking.

Find your site ID

Call GET /v1/rank-tracking/sites and copy the site_id of the website you want.

Read rankings

Call GET /v1/rank-tracking/rankings?site_id=... for current-versus-previous summaries, or GET /v1/rank-tracking/history for a day-by-day series.

Endpoints at a glance

RoutePurposeAuth role
GET /v1/rank-tracking/sitesWebsites with tracking configured, per-engine property and sync freshnessAny role
GET /v1/rank-tracking/keywordsTracked keyword IDs and text for a siteAny role
GET /v1/rank-tracking/rankingsCurrent-vs-previous ranking summaries across enginesAny role
GET /v1/rank-tracking/historyDay-by-day history for one keyword on one engineAny role
GET /v1/rank-tracking/alertsRecorded rank-change alert eventsAny role
GET /v1/rank-tracking/alert-settingsRead a site's alert configurationAny role
PATCH /v1/rank-tracking/alert-settingsUpdate a site's alert configurationOwner, admin or member (not client)

Every route needs your API key in X-API-Key and shares the 60 requests per minute per key rate limit. See Authentication. Responses are plain objects, not wrapped in the usual data / credits_used envelope.

Engines and availability

Bing and Yandex tracking is opt-in per account, so many accounts only have Google connected. Read engine fields carefully:

  • A missing engine key in a site's providers or a keyword's engines means that engine is not connected or attached. It is never a zero or bottom rank. To tell "not enabled for your account" apart from "enabled but not attached to this site", compare against enabled_engines on the rankings response.
  • A present engine with availability other than available can carry null position, clicks and impressions. That is a data-quality or sync-timing signal, not evidence that the keyword ranks nowhere.
  • Positions are provider-reported averages over the returned period, not live, neutral-location SERP ranks.

availability is one of:

ValueMeaning
availableData was synced and is usable
pending_first_syncThe property was attached but has not synced yet
provider_limitedThe provider did not return enough data for this keyword or day. Common for Bing and Yandex, which report a bounded set of queries
no_observed_dataThe sync ran but the provider reported nothing for this keyword in the window
authorization_expiredThe provider connection needs to be reconnected
sync_failedThe most recent sync failed
staleThe stored data is out of date
not_supportedThe provider does not support this for the keyword

Treat every value other than available as a data-quality or timing signal and check data_as_of, last_success_at and error_code before drawing conclusions.

GET /v1/rank-tracking/sites

GET/v1/rank-tracking/sitesFreeAPI key

Lists the websites configured for rank tracking, including each engine's attached primary property and sync freshness.

Example

curl "https://api.rankparse.com/v1/rank-tracking/sites" \
  -H "X-API-Key: rp_your_key"
import requests

res = requests.get(
    "https://api.rankparse.com/v1/rank-tracking/sites",
    headers={"X-API-Key": "rp_your_key"},
)
for site in res.json()["sites"]:
    print(site["site_id"], site["canonical_host"], list(site["providers"]))
const res = await fetch("https://api.rankparse.com/v1/rank-tracking/sites", {
  headers: { "X-API-Key": "rp_your_key" },
});
const { sites } = await res.json();

Response

{
  "sites": [
    {
      "site_id": "site_abc123",
      "canonical_host": "example.com",
      "display_name": "example.com",
      "providers": {
        "google": {
          "engine": "google",
          "property_id": "prop_g1",
          "property_url": "https://example.com/",
          "is_active": true,
          "last_synced_at": 1756112400000,
          "data_as_of": "2026-08-25",
          "latest_sync_status": "succeeded",
          "latest_sync_error_code": null,
          "search_appearance": {
            "as_of_date": "2026-08-25",
            "entries": [
              { "appearance": "AMP_BLUE_LINK", "clicks": 3, "impressions": 120, "ctr": 0.025, "position": 8.4 }
            ]
          },
          "connection": { "label": "you@example.com", "status": "active" }
        }
      }
    }
  ]
}

Response fields

FieldTypeDescription
sites[].site_idstringPass this as site_id to every other rank-tracking route
sites[].canonical_hoststringThe website's host
sites[].display_namestringDisplay name
sites[].providersobjectKeyed by google, bing, yandex. Only engines with an attached primary property appear
providers.*.property_id, property_urlstringThe attached primary property
providers.*.is_activebooleanWhether the property is active
providers.*.last_synced_atinteger or nullUnix time in milliseconds of the last sync
providers.*.data_as_ofstring or nullYYYY-MM-DD the stored data is good through
providers.*.latest_sync_statusstring or nullStatus of the latest sync run: queued, running, succeeded, partial or failed
providers.*.latest_sync_error_codestring or nullStable error code of the latest run. Raw provider errors are never exposed
providers.*.search_appearanceobject or nullThe property's latest stored search-appearance breakdown: as_of_date and entries (appearance, clicks, impressions, ctr, position). null when it has never been synced; it comes from Google Search Console properties
providers.*.connectionobject or nullThe connected account the property is read through (label, status). null when unbound, and always null for roles that cannot view connections

A missing key (no bing above) means Bing is not connected for this site, not that it has zero rankings.

GET /v1/rank-tracking/keywords

GET/v1/rank-tracking/keywordsFreeAPI key

Lists the tracked keyword IDs and query text for one site.

Parameters

ParameterTypeRequiredDefaultDescription
site_idstringYesFrom GET /v1/rank-tracking/sites
limitintegerNo100Keywords per page, 1 to 100. An invalid or out-of-range value falls back to 100
offsetintegerNo0Pagination offset. An invalid value falls back to 0

Example

curl "https://api.rankparse.com/v1/rank-tracking/keywords?site_id=site_abc123&limit=50" \
  -H "X-API-Key: rp_your_key"
import requests

res = requests.get(
    "https://api.rankparse.com/v1/rank-tracking/keywords",
    headers={"X-API-Key": "rp_your_key"},
    params={"site_id": "site_abc123", "limit": 50},
)
print(res.json()["keywords"])
const params = new URLSearchParams({ site_id: "site_abc123", limit: "50" });
const res = await fetch(`https://api.rankparse.com/v1/rank-tracking/keywords?${params}`, {
  headers: { "X-API-Key": "rp_your_key" },
});
const { keywords } = await res.json();

Response

{
  "site_id": "site_abc123",
  "canonical_host": "example.com",
  "keywords": [
    { "id": "kw_1", "query": "buy widgets", "created_at": 1753900800000 }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

created_at is Unix time in milliseconds. total is the number of tracked keywords on the site, so page with offset until you have them all.

GET /v1/rank-tracking/rankings

GET/v1/rank-tracking/rankingsFreeAPI key

Returns current-versus-previous-period ranking summaries for a site's tracked keywords, across every engine enabled for your account by default.

Parameters

ParameterTypeRequiredDefaultDescription
site_idstringYesFrom GET /v1/rank-tracking/sites
keyword_idstringNoRestrict to one tracked keyword
enginestringNoallall, google, bing or yandex. all covers every engine enabled for your account
period_daysintegerNo28Length of the current and previous comparison windows in days, 1 to 90
limitintegerNo100Keywords per page, 1 to 100. An invalid or out-of-range value falls back to 100
offsetintegerNo0Pagination offset

Example

curl "https://api.rankparse.com/v1/rank-tracking/rankings?site_id=site_abc123&engine=all&period_days=28" \
  -H "X-API-Key: rp_your_key"
import requests

res = requests.get(
    "https://api.rankparse.com/v1/rank-tracking/rankings",
    headers={"X-API-Key": "rp_your_key"},
    params={"site_id": "site_abc123", "engine": "all", "period_days": 28},
)
body = res.json()
for kw in body["keywords"]:
    google = kw["engines"].get("google")
    if google and google["availability"] == "available":
        print(kw["query"], google["current"]["position"], google["movement"])
const params = new URLSearchParams({
  site_id: "site_abc123",
  engine: "all",
  period_days: "28",
});
const res = await fetch(`https://api.rankparse.com/v1/rank-tracking/rankings?${params}`, {
  headers: { "X-API-Key": "rp_your_key" },
});
const { keywords, enabled_engines } = await res.json();

To look at one keyword, add &keyword_id=kw_1. To read a single engine, set engine=google, bing or yandex.

Response

{
  "site_id": "site_abc123",
  "canonical_host": "example.com",
  "enabled_engines": ["google", "bing", "yandex"],
  "period_days": 28,
  "limit": 100,
  "offset": 0,
  "total": 1,
  "keywords": [
    {
      "id": "kw_1",
      "query": "buy widgets",
      "engines": {
        "google": {
          "engine": "google",
          "property_id": "prop_g1",
          "property_url": "https://example.com/",
          "availability": "available",
          "data_as_of": "2026-08-25",
          "top_page": "https://example.com/widgets",
          "current_period": { "start_date": "2026-07-29", "end_date": "2026-08-25" },
          "previous_period": { "start_date": "2026-07-01", "end_date": "2026-07-28" },
          "current": { "clicks": 12, "impressions": 340, "ctr": 0.035, "position": 6.2, "click_position": 5.9 },
          "previous": { "clicks": 9, "impressions": 310, "ctr": 0.029, "position": 7.1, "click_position": 6.8 },
          "movement": 0.9,
          "countries": [
            { "country": "usa", "clicks": 8, "impressions": 210, "ctr": 0.038, "position": 5.8 },
            { "country": "gbr", "clicks": 2, "impressions": 60, "ctr": 0.033, "position": 7.0 }
          ]
        },
        "bing": {
          "engine": "bing",
          "property_id": "prop_b1",
          "property_url": "https://example.com/",
          "availability": "provider_limited",
          "data_as_of": "2026-08-20",
          "top_page": null,
          "current_period": { "start_date": "2026-07-29", "end_date": "2026-08-25" },
          "previous_period": { "start_date": "2026-07-01", "end_date": "2026-07-28" },
          "current": { "clicks": null, "impressions": null, "ctr": null, "position": null, "click_position": null },
          "previous": { "clicks": null, "impressions": null, "ctr": null, "position": null, "click_position": null },
          "movement": null,
          "countries": null
        }
      }
    }
  ]
}

Response fields

FieldTypeDescription
enabled_enginesstring[]Engines enabled for your account, whether or not they have data for these keywords
period_days, limit, offsetintegerEcho of the values used
totalintegerMatching keywords, for pagination
keywords[].id, querystringThe tracked keyword
keywords[].enginesobjectKeyed by engine. A missing engine means not connected or attached for this site
engines.*.property_id, property_urlstringThe primary property the numbers come from
engines.*.availabilitystringSee Engines and availability
engines.*.data_as_ofstring or nullYYYY-MM-DD the data is good through
engines.*.top_pagestring or nullThe page the provider reports as top for the query, when stored
engines.*.current_period, previous_periodobjectstart_date and end_date of each window
engines.*.current, previousobjectclicks, impressions, ctr (a 0 to 1 fraction), position (average) and click_position for that window. Each can be null
engines.*.movementnumber or nullprevious.position minus current.position. Positive means the keyword moved up (a lower, better position number). null unless both periods have a position
engines.*.countriesarray or nullUp to 10 countries, by impressions, from the keyword's most recent stored sync period, with country, clicks, impressions, ctr and position. country is the provider's code. null means country data has never been synced for this keyword, which is different from "no traffic"

In the example, bing is provider_limited with entirely null metrics. That is a data-quality gap for the period, not a signal that the keyword does not rank on Bing. yandex is absent from this keyword's engines even though it appears in enabled_engines: Yandex is enabled for the account but not attached to this website.

GET /v1/rank-tracking/history

GET/v1/rank-tracking/historyFreeAPI key

Returns day-by-day history for one keyword on one engine over a date range. History is always single-engine: engine is required and all is not accepted. Call rankings first to compare engines.

Parameters

ParameterTypeRequiredDefaultDescription
site_idstringYesFrom GET /v1/rank-tracking/sites
keyword_idstringYesFrom GET /v1/rank-tracking/keywords
enginestringYesgoogle, bing or yandex. Must be enabled for your account
start_datestringYesYYYY-MM-DD, inclusive
end_datestringYesYYYY-MM-DD, inclusive. Must be a real calendar date on or after start_date; the inclusive span is capped at 400 days
dimensionstringNototaltotal returns one row per day. device returns one row per day per device

Example

curl "https://api.rankparse.com/v1/rank-tracking/history?site_id=site_abc123&keyword_id=kw_1&engine=google&start_date=2026-08-01&end_date=2026-08-25" \
  -H "X-API-Key: rp_your_key"
import requests

res = requests.get(
    "https://api.rankparse.com/v1/rank-tracking/history",
    headers={"X-API-Key": "rp_your_key"},
    params={
        "site_id": "site_abc123",
        "keyword_id": "kw_1",
        "engine": "google",
        "start_date": "2026-08-01",
        "end_date": "2026-08-25",
        "dimension": "device",
    },
)
for row in res.json()["rows"]:
    print(row["date"], row["device"], row["position"])
const params = new URLSearchParams({
  site_id: "site_abc123",
  keyword_id: "kw_1",
  engine: "google",
  start_date: "2026-08-01",
  end_date: "2026-08-25",
});
const res = await fetch(`https://api.rankparse.com/v1/rank-tracking/history?${params}`, {
  headers: { "X-API-Key": "rp_your_key" },
});
const { rows } = await res.json();

Response

{
  "site_id": "site_abc123",
  "keyword_id": "kw_1",
  "query": "buy widgets",
  "engine": "google",
  "property_id": "prop_g1",
  "property_url": "https://example.com/",
  "availability": "available",
  "data_as_of": "2026-08-25",
  "last_attempt_at": 1756112400000,
  "last_success_at": 1756112400000,
  "error_code": null,
  "start_date": "2026-08-01",
  "end_date": "2026-08-25",
  "dimension": "total",
  "rows": [
    { "date": "2026-08-01", "clicks": 1, "impressions": 30, "ctr": 0.033, "position": 6.8, "click_position": 6.5 },
    { "date": "2026-08-03", "clicks": 0, "impressions": 25, "ctr": 0, "position": 7.2, "click_position": null }
  ]
}

Response fields

FieldTypeDescription
availability, data_as_ofstring or nullSame meaning as on rankings, for this keyword and engine
last_attempt_at, last_success_atinteger or nullUnix time in milliseconds of the last sync attempt and last successful sync
error_codestring or nullStable error code of the last failed attempt
dimensionstringtotal or device, echoing the request
rowsarrayAscending by date (then by device)
rows[].datestringYYYY-MM-DD
rows[].clicks, impressions, ctr, position, click_positionnumber or nullDaily metrics
rows[].devicestringdesktop, mobile or tablet. Present only when dimension=device
  • A missing calendar date (2026-08-02 above) means there was no synced observation that day. Never treat a gap as a zero or a dropped rank.
  • Device rows exist only where the provider synced them: always for Google, and for Yandex only when its per-device breakdown is switched on. An empty device history next to a populated total history means "not synced per device", not "no traffic". Bing has no device breakdown.

GET /v1/rank-tracking/alerts

GET/v1/rank-tracking/alertsFreeAPI key

Lists recorded rank-change alert events for one site, newest first. Events are recorded daily while alerts are enabled for the site (see Alert settings). This is the full history, including changes that were left out of the weekly email because their net movement cancelled out.

Parameters

ParameterTypeRequiredDefaultDescription
site_idstringYesFrom GET /v1/rank-tracking/sites
limitintegerNo100Events per page, 1 to 100. An invalid value falls back to 100
offsetintegerNo0Pagination offset
curl "https://api.rankparse.com/v1/rank-tracking/alerts?site_id=site_abc123" \
  -H "X-API-Key: rp_your_key"
import requests

res = requests.get(
    "https://api.rankparse.com/v1/rank-tracking/alerts",
    headers={"X-API-Key": "rp_your_key"},
    params={"site_id": "site_abc123"},
)
for alert in res.json()["alerts"]:
    print(alert["metric_date"], alert["query"], alert["alert_type"])
const res = await fetch(
  "https://api.rankparse.com/v1/rank-tracking/alerts?site_id=site_abc123",
  { headers: { "X-API-Key": "rp_your_key" } },
);
const { alerts } = await res.json();

Response

{
  "site_id": "site_abc123",
  "total": 1,
  "limit": 100,
  "offset": 0,
  "alerts": [
    {
      "keyword_id": "kw_1",
      "query": "buy widgets",
      "engine": "google",
      "property_id": "prop_g1",
      "metric_date": "2026-08-25",
      "alert_type": "entered_top_10",
      "old_position": 12.4,
      "new_position": 8.9,
      "detected_at": 1756166400000,
      "digest_date": "2026-08-26",
      "delivered_at": null
    }
  ]
}

Response fields

FieldTypeDescription
keyword_id, querystringThe keyword
engine, property_idstringEngine and property the change was seen on
metric_datestringThe synced day the change was measured on
alert_typestringSee the table below
old_positionnumber or nullThe keyword's average position over the prior 7-day baseline. null for new_ranking
new_positionnumber or nullPosition on the freshest synced day. null for lost_ranking
detected_atintegerUnix time in milliseconds when it was recorded
digest_datestringThe UTC date of the detection pass that recorded the event
delivered_atinteger or nullWhen it was included in a sent weekly email. null while pending, and for events that were cancelled out or never emailed

Alert types

Detection compares each keyword's position on the property's freshest synced day with its average position over the prior 7 days, per engine.

alert_typeRecorded when
entered_top_3Now in positions 1 to 3, baseline outside them
left_top_3Now outside positions 1 to 3 but still in the top 10, baseline inside the top 3
entered_top_10Now in positions 4 to 10, baseline outside the top 10
left_top_10Now outside the top 10, baseline inside it
moved_upAverage position improved by at least the site's move_threshold places
moved_downAverage position worsened by at least move_threshold places
new_rankingThe keyword has a position now and had none in the baseline
lost_rankingThe keyword had a baseline position and has none now, and the provider's data covers the freshest day

Only the most specific boundary crossing is recorded per direction (entering the top 3 implies entering the top 10, so only entered_top_3 is recorded). moved_up and moved_down are recorded independently of boundary events. Only keyword and engine pairs whose availability is available generate events, and a null position is never treated as a move.

Alert settings

GET/v1/rank-tracking/alert-settingsFreeAPI key

Alerts are opt-in and off by default, set per website. Read the settings with GET, change them with PATCH. Both take site_id as a query parameter.

GET response

{
  "site_id": "site_abc123",
  "enabled": false,
  "email_override": null,
  "move_threshold": 5,
  "last_detected_date": null
}
FieldTypeDescription
enabledbooleanWhether detection and the weekly email are on for this site. Default false
email_overridestring or nullAddress that receives this site's lines instead of your account email. null uses the account email
move_thresholdnumberPosition places that make a moved_up or moved_down event. Default 5
last_detected_datestring or nullThe last UTC date detection ran for this site
PATCH/v1/rank-tracking/alert-settingsFreeAPI key

PATCH merges only the fields you send; anything you omit keeps its current value. Send site_id in the query string, the fields in a JSON body.

Body fieldTypeRules
enabledbooleantrue or false
move_thresholdnumberFrom 1 to 50
email_overridestring or nullA valid email address, or null to go back to the account email
curl -X PATCH "https://api.rankparse.com/v1/rank-tracking/alert-settings?site_id=site_abc123" \
  -H "X-API-Key: rp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "move_threshold": 5}'
import requests

res = requests.patch(
    "https://api.rankparse.com/v1/rank-tracking/alert-settings",
    headers={"X-API-Key": "rp_your_key"},
    params={"site_id": "site_abc123"},
    json={"enabled": True, "move_threshold": 5},
)
print(res.json())
const res = await fetch(
  "https://api.rankparse.com/v1/rank-tracking/alert-settings?site_id=site_abc123",
  {
    method: "PATCH",
    headers: {
      "X-API-Key": "rp_your_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ enabled: true, move_threshold: 5 }),
  },
);
console.log(await res.json());

The response is the full settings object, in the same shape as GET.

Turning enabled on starts a real weekly email to the account address (or email_override) whenever meaningful changes exist. Confirm with the account owner before enabling it on someone else's behalf.

Weekly rank-change emails

Detection runs daily for each site with alerts enabled. Emails go out on Sundays at 18:00 UTC, and only when meaningful changes remain after consolidation.

  • Opt-in per website. Nothing is sent until you set enabled to true.
  • One email per recipient. If you track several websites, each recipient gets at most one digest per week covering all of their sites. A per-site email_override can split sites across different recipients.
  • One net change per keyword. Repeated moves for the same website, keyword and engine in the week are shown as a single net change. Moves that cancel out, or that fall below the site's move_threshold, are left out.
  • No meaningful changes means no email.
  • Failed sends are retried. If the email service does not accept a send, those events stay pending and are included in the next weekly run.
  • Positions in the email are provider-reported averages, not live SERP checks. Digests never contain tokens or API keys.
  • Events older than 400 days are pruned.

The dashboard shows each website's alert state read-only under its Settings, in the Rank alerts section. Change it with the API or the MCP tool below.

Behavior and limits

  • All routes cost 0 credits and are limited only by the 60 requests per minute rate limit.
  • site_id and keyword_id are scoped to your workspace. An ID that belongs to someone else returns the same 404 as one that does not exist.
  • limit and offset on keywords, rankings and alerts: invalid or out-of-range values fall back to the defaults instead of returning an error. period_days is validated strictly.

Errors

All errors use the standard shape { "error", "code", "message" }. See Error codes.

StatusCodeMeaning
400invalid_rank_tracking_engineengine is not one of the allowed values, or on /history it is missing or all
400invalid_rank_tracking_periodperiod_days is not an integer from 1 to 90
400invalid_rank_tracking_datestart_date or end_date is missing, malformed, not a real date, out of order, or spans more than 400 days
400invalid_rank_tracking_dimensiondimension is not total or device
400invalid_rank_tracking_alert_settingsThe PATCH body is not a JSON object, enabled is not a boolean, move_threshold is outside 1 to 50, or email_override is not an email or null
400rank_tracking_provider_disabledengine is a real engine that is not enabled for your account
400rank_tracking_property_not_attached/history only: the keyword has no primary property attached for that engine
401invalid_api_keyMissing, invalid or revoked API key
403capability_missingYour workspace role cannot make this call (for example PATCH as a client)
404rank_tracking_site_not_foundNo site with that site_id in your workspace
404rank_tracking_keyword_not_foundNo keyword with that keyword_id on this site
429rate_limitedMore than 60 requests per minute for this key

MCP tools

All seven tools are on the API-key MCP endpoint (mcp.rankparse.com) only. The secure OAuth connector does not include them. Reads cost 0 credits. See MCP Server.

ToolRouteNotes
list_rank_tracking_sitesGET /sitesRead-only
list_tracked_keywordsGET /keywordsRead-only
get_keyword_rankingsGET /rankingsRead-only
get_keyword_rank_historyGET /historyRead-only. Takes the optional dimension
list_rank_change_alertsGET /alertsRead-only
get_rank_alert_settingsGET /alert-settingsRead-only
update_rank_alert_settingsPATCH /alert-settingsWrites; safe to repeat. Takes enabled, move_threshold and email_override

Distinct from GSC and Google Ads

Rank tracking, Google Search Console and Google Ads are three separate data products. None of them changes the others:

  • Google Search Console endpoints expose live, Google-only Search Console dimensions: pages, devices, countries, sitemaps and URL inspection.
  • Google Ads keyword research returns search volume and competition, never organic rankings, for any engine.
  • Rank tracking is the only family that reads stored Google, Bing and Yandex position snapshots side by side.

On this page