Batch overnight rank checking lets you track thousands of keywords without burning daytime API quota or slowing down production systems. The pattern is straightforward: load keywords from a CSV, loop through them with a short delay, store positions in a results file, and schedule the script with cron. Using the Scavio API at $0.005 per credit, a 4,000-keyword nightly run costs $20 -- compared to $60+ on SerpAPI or $499.95+ on Semrush Business. This tutorial builds a complete overnight batch pipeline in Python.
Prerequisites
- Python 3.9+ installed
- requests and csv modules (both in stdlib except requests)
- A Scavio API key from scavio.dev
- A CSV file with keywords to track
Walkthrough
Step 1: Prepare your keyword CSV
Create a CSV with columns for keyword and target_url. The script will check each keyword and record where the target URL ranks.
import csv
keywords = [
{'keyword': 'best crm for startups', 'target_url': 'yoursite.com'},
{'keyword': 'crm pricing comparison 2026', 'target_url': 'yoursite.com'},
{'keyword': 'hubspot alternative small business', 'target_url': 'yoursite.com'},
]
with open('keywords.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['keyword', 'target_url'])
writer.writeheader()
writer.writerows(keywords)
print(f'Wrote {len(keywords)} keywords')Step 2: Build the rank check function
For each keyword, call the search API and scan organic results for your target domain. Return the position or None if not found in the top results.
import requests, os
API_KEY = os.environ['SCAVIO_API_KEY']
def check_rank(keyword: str, target_domain: str) -> dict:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': API_KEY, 'Content-Type': 'application/json'},
json={'query': keyword, 'country_code': 'us'})
resp.raise_for_status()
for r in resp.json().get('organic_results', []):
if target_domain in r.get('link', ''):
return {'keyword': keyword, 'position': r['position'], 'url': r['link']}
return {'keyword': keyword, 'position': None, 'url': None}Step 3: Batch through all keywords with rate limiting
Read the CSV, check each keyword with a 0.5-second delay to stay well within rate limits, and collect results. For 4,000 keywords this takes about 35 minutes.
import time
def batch_rank_check(csv_path: str) -> list:
results = []
with open(csv_path) as f:
reader = csv.DictReader(f)
rows = list(reader)
print(f'Checking {len(rows)} keywords...')
for i, row in enumerate(rows):
result = check_rank(row['keyword'], row['target_url'])
results.append(result)
if (i + 1) % 100 == 0:
print(f' Checked {i + 1}/{len(rows)}')
time.sleep(0.5)
return resultsStep 4: Save results with timestamp
Write results to a dated CSV so you can track rank changes over time. Each nightly run produces one file.
from datetime import date
def save_results(results: list) -> str:
filename = f'ranks_{date.today().isoformat()}.csv'
with open(filename, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['keyword', 'position', 'url'])
writer.writeheader()
writer.writerows(results)
ranked = [r for r in results if r['position'] is not None]
print(f'Saved {len(results)} results to {filename}')
print(f' Ranking: {len(ranked)}, Not found: {len(results) - len(ranked)}')
return filenameStep 5: Schedule with cron for nightly runs
Add a cron entry to run the script at 2 AM daily. The script finishes well before morning so results are ready when you start work.
# Add to crontab (crontab -e):
# 0 2 * * * cd /path/to/project && SCAVIO_API_KEY=your_key python batch_ranks.py
# Full script entry point:
if __name__ == '__main__':
results = batch_rank_check('keywords.csv')
save_results(results)
cost = len(results) * 0.005
print(f'Total cost: ${cost:.2f}')Python Example
import os, csv, time, requests
from datetime import date
API_KEY = os.environ['SCAVIO_API_KEY']
ENDPOINT = 'https://api.scavio.dev/api/v1/search'
def check_rank(keyword: str, target: str) -> dict:
resp = requests.post(ENDPOINT,
headers={'x-api-key': API_KEY, 'Content-Type': 'application/json'},
json={'query': keyword, 'country_code': 'us'})
resp.raise_for_status()
for r in resp.json().get('organic_results', []):
if target in r.get('link', ''):
return {'keyword': keyword, 'position': r['position'], 'url': r['link']}
return {'keyword': keyword, 'position': None, 'url': None}
def main():
with open('keywords.csv') as f:
rows = list(csv.DictReader(f))
results = []
for i, row in enumerate(rows):
results.append(check_rank(row['keyword'], row['target_url']))
if (i + 1) % 100 == 0:
print(f'Checked {i + 1}/{len(rows)}')
time.sleep(0.5)
filename = f'ranks_{date.today()}.csv'
with open(filename, 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=['keyword', 'position', 'url'])
w.writeheader()
w.writerows(results)
ranked = sum(1 for r in results if r['position'])
print(f'{ranked}/{len(results)} ranking, cost: ${len(results) * 0.005:.2f}')
if __name__ == '__main__':
main()JavaScript Example
const fs = require('fs');
const API_KEY = process.env.SCAVIO_API_KEY;
async function checkRank(keyword, target) {
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: keyword, country_code: 'us' })
});
const data = await resp.json();
const match = (data.organic_results || []).find(r => r.link?.includes(target));
return { keyword, position: match?.position || null, url: match?.link || null };
}
async function main() {
const lines = fs.readFileSync('keywords.csv', 'utf8').trim().split('\n').slice(1);
const results = [];
for (const line of lines) {
const [keyword, target] = line.split(',');
results.push(await checkRank(keyword, target));
await new Promise(r => setTimeout(r, 500));
}
const ranked = results.filter(r => r.position).length;
console.log(`${ranked}/${results.length} ranking, cost: $${(results.length * 0.005).toFixed(2)}`);
}
main().catch(console.error);Expected Output
Checking 4000 keywords...
Checked 100/4000
Checked 200/4000
...
Checked 4000/4000
Saved 4000 results to ranks_2026-05-13.csv
Ranking: 2847, Not found: 1153
Total cost: $20.00