To run a bulk SERP ranking study, collect a list of keywords, fetch the organic results for each one with POST /api/v1/google, pull the feature you care about (say URL character length) at every ranking position, then aggregate by position bucket and compute a correlation. That's it. A German SEO once tested whether short URLs rank better across 27,000 SERPs and the effect was tiny. You can settle questions like that with real data instead of a hot take. This tutorial walks the whole loop: keyword list, paced API calls, feature extraction, aggregation, and a Pearson correlation. Correlation isn't causation, and SERPs have confounders, so read the number as a hint, not proof.
Prerequisites
- A Scavio API key (50 free credits on signup; each Google call with light_request:true costs 1 credit)
- Python 3.9+ or Node 18+ with the requests/fetch ability to POST JSON
- A list of 50 to a few thousand keywords in your niche (a CSV or plain text file)
- Basic comfort reading a correlation coefficient (a number between -1 and 1)
Walkthrough
Step 1: Build your keyword list
A study is only as honest as its sample. Pull real keywords from your niche rather than cherry-picking. Load them from a file so the run is reproducible. Aim for at least a few hundred if you want a stable correlation.
# keywords.txt has one query per line
with open('keywords.txt') as f:
keywords = [line.strip() for line in f if line.strip()]
print(f'Loaded {len(keywords)} keywords')Step 2: Fetch one SERP and inspect the shape
Before looping over thousands, call the endpoint once and look at the response. Use light_request:true to keep each call at 1 credit. You get organic results, each with position, title, link, and snippet.
import requests
API_KEY = 'YOUR_API_KEY'
resp = requests.post(
'https://api.scavio.dev/api/v1/google',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'query': 'best running shoes', 'light_request': True},
)
data = resp.json()
for r in data['organic_results'][:3]:
print(r['position'], r['link'])Step 3: Extract the feature per position
Here the feature is URL character length, but swap in title length, domain depth, or HTTPS presence as your hypothesis demands. Record one (position, value) pair per result so you can correlate later.
def url_length(result):
return len(result['link'])
def extract_pairs(serp):
return [(r['position'], url_length(r)) for r in serp['organic_results']]Step 4: Loop over keywords and respect the rate limit
Free keys allow 1 request per second; the $30 plan allows 2. Sleep between calls so you don't get throttled. Wrap each call in try/except so one bad query doesn't kill the whole run.
import time
all_pairs = []
for kw in keywords:
try:
resp = requests.post(
'https://api.scavio.dev/api/v1/google',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'query': kw, 'light_request': True},
)
all_pairs += extract_pairs(resp.json())
except Exception as e:
print(f'skip {kw}: {e}')
time.sleep(1.0) # 1 req/sec on the free planStep 5: Aggregate by position bucket
Group results into buckets (1-3, 4-6, 7-10) and average the feature in each. This shows the trend at a glance before you trust any single number.
from collections import defaultdict
buckets = defaultdict(list)
for pos, val in all_pairs:
if pos <= 3: buckets['1-3'].append(val)
elif pos <= 6: buckets['4-6'].append(val)
else: buckets['7-10'].append(val)
for name in ['1-3', '4-6', '7-10']:
vals = buckets[name]
print(name, round(sum(vals)/len(vals), 1))Step 6: Compute the correlation
A Pearson coefficient between position and the feature tells you the direction and strength. Near 0 means no relationship; the German URL-length study landed close to 0. Remember: confounders abound and correlation isn't causation.
import statistics
def pearson(xs, ys):
mx, my = statistics.mean(xs), statistics.mean(ys)
num = sum((x-mx)*(y-my) for x, y in zip(xs, ys))
den = (sum((x-mx)**2 for x in xs) * sum((y-my)**2 for y in ys)) ** 0.5
return num/den if den else 0.0
positions = [p for p, _ in all_pairs]
values = [v for _, v in all_pairs]
print('correlation:', round(pearson(positions, values), 3))Python Example
import time
import statistics
from collections import defaultdict
import requests
API_KEY = 'YOUR_API_KEY'
ENDPOINT = 'https://api.scavio.dev/api/v1/google'
def fetch_serp(query):
resp = requests.post(
ENDPOINT,
headers={'Authorization': f'Bearer {API_KEY}'},
json={'query': query, 'light_request': True},
)
resp.raise_for_status()
return resp.json()
def url_length(result):
return len(result['link'])
def pearson(xs, ys):
mx, my = statistics.mean(xs), statistics.mean(ys)
num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
den = (sum((x - mx) ** 2 for x in xs) * sum((y - my) ** 2 for y in ys)) ** 0.5
return num / den if den else 0.0
def main():
with open('keywords.txt') as f:
keywords = [line.strip() for line in f if line.strip()]
print(f'Loaded {len(keywords)} keywords')
pairs = []
for kw in keywords:
try:
serp = fetch_serp(kw)
pairs += [(r['position'], url_length(r)) for r in serp['organic_results']]
except Exception as e:
print(f'skip {kw}: {e}')
time.sleep(1.0) # 1 req/sec on the free plan; 0.5 on the $30 plan
buckets = defaultdict(list)
for pos, val in pairs:
if pos <= 3:
buckets['1-3'].append(val)
elif pos <= 6:
buckets['4-6'].append(val)
else:
buckets['7-10'].append(val)
print('avg URL length by position bucket:')
for name in ['1-3', '4-6', '7-10']:
vals = buckets[name]
if vals:
print(f' {name}: {round(sum(vals) / len(vals), 1)}')
positions = [p for p, _ in pairs]
values = [v for _, v in pairs]
print('correlation (position vs url length):', round(pearson(positions, values), 3))
if __name__ == '__main__':
main()JavaScript Example
import fs from 'node:fs';
const API_KEY = 'YOUR_API_KEY';
const ENDPOINT = 'https://api.scavio.dev/api/v1/google';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function fetchSerp(query) {
const resp = await fetch(ENDPOINT, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, light_request: true }),
});
return resp.json();
}
function pearson(xs, ys) {
const mx = xs.reduce((a, b) => a + b, 0) / xs.length;
const my = ys.reduce((a, b) => a + b, 0) / ys.length;
let num = 0, dx = 0, dy = 0;
for (let i = 0; i < xs.length; i++) {
num += (xs[i] - mx) * (ys[i] - my);
dx += (xs[i] - mx) ** 2;
dy += (ys[i] - my) ** 2;
}
const den = Math.sqrt(dx * dy);
return den ? num / den : 0;
}
const keywords = fs.readFileSync('keywords.txt', 'utf8').split('\n').map((s) => s.trim()).filter(Boolean);
const pairs = [];
for (const kw of keywords) {
try {
const serp = await fetchSerp(kw);
for (const r of serp.organic_results) pairs.push([r.position, r.link.length]);
} catch (e) {
console.log(`skip ${kw}: ${e}`);
}
await sleep(1000); // 1 req/sec on the free plan
}
const positions = pairs.map((p) => p[0]);
const values = pairs.map((p) => p[1]);
console.log('correlation:', pearson(positions, values).toFixed(3));Expected Output
Loaded 500 keywords
avg URL length by position bucket:
1-3: 48.2
4-6: 51.7
7-10: 53.9
correlation (position vs url length): 0.072