Workflow

Weekly Rank Tracking API Pipeline

Track 200 keywords weekly with Scavio, diff position changes against history, and alert on drops exceeding your threshold.

Overview

This workflow tracks up to 200 target keywords every week, records your domain's position in Google search results, diffs against the previous week's positions, and fires alerts when rankings drop beyond a configurable threshold. It produces a weekly report with gainers, losers, and stable keywords, feeding directly into SEO dashboards or Slack digests.

Trigger

Cron schedule (every Monday at 7 AM UTC)

Schedule

Runs every Monday at 7 AM UTC

Workflow Steps

1

Load keyword list

Read the target keywords and domain from a configuration file.

2

Query Google for each keyword

Call the Scavio API with platform google for each keyword, requesting 30 results to cover page one and two.

3

Extract domain positions

Scan organic results for each keyword and record the position where the target domain appears.

4

Diff against previous week

Load last week's positions and compute deltas for each keyword.

5

Categorize changes

Sort keywords into gainers, losers, stable, and not-found buckets based on threshold.

6

Generate report and alert

Build a structured report, save to disk, and send alert for any significant drops.

Python Implementation

Python
import requests
import json
import time
from pathlib import Path
from datetime import datetime

API_KEY = "your_scavio_api_key"
DOMAIN = "yourdomain.com"
DROP_THRESHOLD = 3
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

def get_position(keyword: str) -> int | None:
    res = requests.post(
        "https://api.scavio.dev/api/v1/search",
        headers={"x-api-key": API_KEY},
        json={"platform": "google", "query": keyword, "num": 30},
        timeout=15,
    )
    res.raise_for_status()
    for item in res.json().get("organic", []):
        if DOMAIN in item.get("link", ""):
            return item["position"]
    return None

def run():
    keywords = json.loads(Path("keywords.json").read_text())
    history_path = Path("rank_history.json")
    previous = json.loads(history_path.read_text()) if history_path.exists() else {}

    current_positions = {}
    gainers = []
    losers = []
    stable = []

    for i, kw in enumerate(keywords):
        pos = get_position(kw)
        current_positions[kw] = pos
        prev = previous.get(kw)

        if pos and prev:
            delta = prev - pos  # positive = improved
            if delta >= DROP_THRESHOLD:
                gainers.append({"keyword": kw, "from": prev, "to": pos, "change": delta})
            elif delta <= -DROP_THRESHOLD:
                losers.append({"keyword": kw, "from": prev, "to": pos, "change": delta})
            else:
                stable.append(kw)

        if i % 20 == 19:
            time.sleep(1)

    # Save current as new baseline
    history_path.write_text(json.dumps(current_positions, indent=2))

    report = {
        "date": datetime.utcnow().strftime("%Y-%m-%d"),
        "total_keywords": len(keywords),
        "tracked": sum(1 for v in current_positions.values() if v),
        "gainers": gainers,
        "losers": losers,
        "stable_count": len(stable),
    }

    Path(f"rank_report_{report['date']}.json").write_text(json.dumps(report, indent=2))

    if losers:
        msg = f"Rank Drop Alert: {len(losers)} keywords dropped\n"
        for l in losers[:10]:
            msg += f"  {l['keyword']}: #{l['from']} -> #{l['to']}\n"
        requests.post(SLACK_WEBHOOK, json={"text": msg}, timeout=10)

    print(f"Report: {len(gainers)} up, {len(losers)} down, {len(stable)} stable")

if __name__ == "__main__":
    run()

JavaScript Implementation

JavaScript
const API_KEY = "your_scavio_api_key";
const DOMAIN = "yourdomain.com";
const DROP_THRESHOLD = 3;
const SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL";

async function getPosition(keyword) {
  const res = await fetch("https://api.scavio.dev/api/v1/search", {
    method: "POST",
    headers: { "x-api-key": API_KEY, "content-type": "application/json" },
    body: JSON.stringify({ platform: "google", query: keyword, num: 30 }),
  });
  if (!res.ok) throw new Error(`scavio ${res.status}`);
  const data = await res.json();
  for (const item of data.organic ?? []) {
    if (item.link?.includes(DOMAIN)) return item.position;
  }
  return null;
}

async function run() {
  const fs = await import("fs/promises");
  const keywords = JSON.parse(await fs.readFile("keywords.json", "utf8"));
  let previous = {};
  try { previous = JSON.parse(await fs.readFile("rank_history.json", "utf8")); } catch {}

  const currentPositions = {};
  const gainers = [];
  const losers = [];
  const stable = [];

  for (let i = 0; i < keywords.length; i++) {
    const kw = keywords[i];
    const pos = await getPosition(kw);
    currentPositions[kw] = pos;
    const prev = previous[kw];

    if (pos && prev) {
      const delta = prev - pos;
      if (delta >= DROP_THRESHOLD) gainers.push({ keyword: kw, from: prev, to: pos, change: delta });
      else if (delta <= -DROP_THRESHOLD) losers.push({ keyword: kw, from: prev, to: pos, change: delta });
      else stable.push(kw);
    }

    if (i % 20 === 19) await new Promise((r) => setTimeout(r, 1000));
  }

  await fs.writeFile("rank_history.json", JSON.stringify(currentPositions, null, 2));

  const date = new Date().toISOString().slice(0, 10);
  const report = { date, totalKeywords: keywords.length, tracked: Object.values(currentPositions).filter(Boolean).length, gainers, losers, stableCount: stable.length };
  await fs.writeFile(`rank_report_${date}.json`, JSON.stringify(report, null, 2));

  if (losers.length) {
    const msg = `Rank Drop Alert: ${losers.length} keywords dropped\n` +
      losers.slice(0, 10).map((l) => `  ${l.keyword}: #${l.from} -> #${l.to}`).join("\n");
    await fetch(SLACK_WEBHOOK, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: msg }) });
  }

  console.log(`Report: ${gainers.length} up, ${losers.length} down, ${stable.length} stable`);
}

run();

Platforms Used

Google

Web search with knowledge graph, PAA, and AI overviews

Frequently Asked Questions

This workflow tracks up to 200 target keywords every week, records your domain's position in Google search results, diffs against the previous week's positions, and fires alerts when rankings drop beyond a configurable threshold. It produces a weekly report with gainers, losers, and stable keywords, feeding directly into SEO dashboards or Slack digests.

This workflow uses a cron schedule (every monday at 7 am utc). Runs every Monday at 7 AM UTC.

This workflow uses the following Scavio platforms: google. Each platform is called via the same unified API endpoint.

Yes. Scavio's free tier includes 250 credits per month with no credit card required. That is enough to test and validate this workflow before scaling it.

Weekly Rank Tracking API Pipeline

Track 200 keywords weekly with Scavio, diff position changes against history, and alert on drops exceeding your threshold.