How to Track Local SEO Rankings Programmatically
Why Local SEO Tracking Matters
Local search drives real-world action. According to Google's own data, nearly half of all searches have local intent. When someone searches for "plumber near me" or "best pizza in Brooklyn", they are looking for a local business and are likely to visit or call within hours. For businesses that serve local customers, ranking in these searches is directly tied to revenue.
But here is the challenge: search results vary dramatically by location. A search for "dentist" in New York City returns completely different results than the same query in Austin, Texas. If you are an SEO agency managing clients across multiple cities, or a multi-location business tracking your own presence, you need a way to check rankings from different geographic perspectives.
Manual checking is tedious and unreliable. Your personal search history, device, and physical location all influence what you see. Programmatic rank tracking through a SERP API gives you clean, consistent, location-specific results that you can monitor over time.
Challenges of Local Rank Tracking
Local rank tracking presents unique challenges that do not exist in standard SEO monitoring.
Location-Specific Results
Search engines personalize results based on the searcher's location, often down to the city or neighborhood level. Two people in different parts of the same city can see different local pack results. This means you cannot simply run a search and assume the results apply everywhere.
Google My Business Integration
Local search results often include a "local pack" with Google My Business listings. These listings appear above organic results and receive the majority of clicks for local queries. Tracking whether your business appears in the local pack requires analyzing a different part of the SERP than standard organic rankings.
Volatile Rankings
Local rankings tend to fluctuate more than national rankings. A single new review, a competitor opening nearby, or a change in business hours can shift local positions. This volatility means you need to track rankings frequently to distinguish real trends from daily fluctuations.
Multiple Locations
Businesses with multiple locations need to track each location independently. A restaurant chain with locations in five cities needs five separate sets of rank tracking data. This multiplies the number of API calls needed but is essential for understanding performance at each location.
Using the Country Parameter
The Serpent API supports a country parameter that returns search results as seen by users in that country. This is the foundation for local rank tracking:
GET https://apiserpent.com/api/search?q=best+coffee+shop&country=us
Headers:
X-API-Key: sk_live_your_api_key
Available country codes include us, uk, ca, au, in, de, fr, and many more. By specifying the country, you get results that match what users in that region would see, giving you a geographic perspective on your rankings.
Tracking Rankings by Region
To track rankings across multiple regions, you run the same keyword query with different country parameters and compare the results. Here is a function that does exactly this:
async function trackRankingsByRegion(keyword, domain, regions) {
const rankings = {};
for (const region of regions) {
const params = new URLSearchParams({
q: keyword,
num: 50,
country: region
});
const response = await fetch(
`https://apiserpent.com/api/search?${params}`,
{ headers: { 'X-API-Key': 'sk_live_your_api_key' } }
);
const data = await response.json();
if (data.success && data.results.organic) {
const position = data.results.organic.findIndex(
result => result.url.includes(domain)
);
rankings[region] = {
position: position >= 0 ? position + 1 : null,
url: position >= 0 ? data.results.organic[position].url : null,
totalResults: data.results.organic.length,
checkedAt: new Date().toISOString()
};
}
// Rate limiting
await new Promise(r => setTimeout(r, 1500));
}
return rankings;
}
// Track rankings across regions
const rankings = await trackRankingsByRegion(
'project management software',
'example.com',
['us', 'uk', 'ca', 'au', 'in']
);
console.log(rankings);
// { us: { position: 5, ... }, uk: { position: 12, ... }, ... }
Building a Local Rank Tracker
A complete rank tracker needs to handle multiple keywords, store historical data, and detect ranking changes. Here is a full implementation:
const fs = require('fs');
class LocalRankTracker {
constructor(apiKey, dataFile = 'rankings.json') {
this.apiKey = apiKey;
this.dataFile = dataFile;
this.history = this.loadHistory();
}
loadHistory() {
try {
return JSON.parse(fs.readFileSync(this.dataFile, 'utf8'));
} catch {
return { checks: [] };
}
}
saveHistory() {
fs.writeFileSync(this.dataFile, JSON.stringify(this.history, null, 2));
}
async checkRanking(keyword, domain, country = 'us') {
const params = new URLSearchParams({
q: keyword,
num: 100,
country: country
});
const response = await fetch(
`https://apiserpent.com/api/search?${params}`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const data = await response.json();
let position = null;
let matchedUrl = null;
if (data.success && data.results.organic) {
for (let i = 0; i < data.results.organic.length; i++) {
if (data.results.organic[i].url.includes(domain)) {
position = i + 1;
matchedUrl = data.results.organic[i].url;
break;
}
}
}
const check = {
keyword,
domain,
country,
position,
matchedUrl,
timestamp: new Date().toISOString()
};
this.history.checks.push(check);
this.saveHistory();
return check;
}
async trackKeywords(keywords, domain, countries) {
const results = [];
for (const keyword of keywords) {
for (const country of countries) {
const result = await this.checkRanking(keyword, domain, country);
results.push(result);
console.log(
`[${country.toUpperCase()}] "${keyword}": ` +
`${result.position ? '#' + result.position : 'Not found'}`
);
await new Promise(r => setTimeout(r, 1500));
}
}
return results;
}
getHistory(keyword, country) {
return this.history.checks
.filter(c => c.keyword === keyword && c.country === country)
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
}
detectChanges(keyword, country) {
const history = this.getHistory(keyword, country);
if (history.length < 2) return null;
const latest = history[history.length - 1];
const previous = history[history.length - 2];
return {
keyword,
country,
previousPosition: previous.position,
currentPosition: latest.position,
change: previous.position && latest.position
? previous.position - latest.position
: null,
direction: !latest.position ? 'dropped'
: !previous.position ? 'entered'
: latest.position < previous.position ? 'improved'
: latest.position > previous.position ? 'declined'
: 'unchanged'
};
}
}
// Usage
const tracker = new LocalRankTracker('sk_live_your_api_key');
await tracker.trackKeywords(
['best crm software', 'crm for small business', 'free crm tool'],
'example.com',
['us', 'uk', 'in']
);
Monitoring Google My Business Rankings
For local businesses, the Google My Business (GMB) listing is often more important than organic rankings. GMB results appear in the local pack at the top of search results. When using the Serpent API, the response may include local pack data alongside organic results.
To monitor GMB rankings specifically, look for your business name in the local results section of the API response:
async function checkLocalPack(keyword, businessName, country) {
const params = new URLSearchParams({
q: keyword,
num: 10,
country: country
});
const response = await fetch(
`https://apiserpent.com/api/search?${params}`,
{ headers: { 'X-API-Key': 'sk_live_your_api_key' } }
);
const data = await response.json();
// Check organic results for local intent signals
const organicPosition = data.results.organic?.findIndex(
r => r.title.toLowerCase().includes(businessName.toLowerCase())
);
return {
keyword,
country,
inOrganic: organicPosition >= 0,
organicPosition: organicPosition >= 0 ? organicPosition + 1 : null,
checkedAt: new Date().toISOString()
};
}
Complete Code Walkthrough
Let us put it all together into a script that you can run daily to track your local rankings. This script tracks multiple keywords across multiple regions, detects changes, and outputs a summary report:
async function dailyRankCheck() {
const tracker = new LocalRankTracker('sk_live_your_api_key');
const config = {
domain: 'yourdomain.com',
keywords: [
'plumber emergency',
'plumber near me',
'pipe repair service',
'water heater installation'
],
countries: ['us', 'uk']
};
console.log('Starting daily rank check...');
console.log(`Domain: ${config.domain}`);
console.log(`Keywords: ${config.keywords.length}`);
console.log(`Regions: ${config.countries.join(', ')}\n`);
await tracker.trackKeywords(
config.keywords,
config.domain,
config.countries
);
// Generate change report
console.log('\n--- Ranking Changes ---');
for (const keyword of config.keywords) {
for (const country of config.countries) {
const change = tracker.detectChanges(keyword, country);
if (change) {
const arrow = change.direction === 'improved' ? 'UP'
: change.direction === 'declined' ? 'DOWN'
: change.direction;
console.log(
`[${country.toUpperCase()}] "${keyword}": ${arrow} ` +
`(${change.previousPosition || 'N/A'} -> ${change.currentPosition || 'N/A'})`
);
}
}
}
}
dailyRankCheck();
Scheduling Checks with Cron
For continuous monitoring, schedule your rank tracking script to run automatically. On Linux and macOS, use cron to run the script at regular intervals:
# Edit crontab
crontab -e
# Run rank check every day at 6:00 AM
0 6 * * * /usr/bin/node /path/to/rank-tracker.js >> /var/log/rank-tracker.log 2>&1
# Run every 12 hours for more frequent monitoring
0 */12 * * * /usr/bin/node /path/to/rank-tracker.js >> /var/log/rank-tracker.log 2>&1
For cloud deployments, consider using GitHub Actions, AWS Lambda with CloudWatch Events, or a managed cron service. The key is consistency: run your checks at the same time each day so you can compare apples to apples when analyzing trends.
Analyzing Ranking Data Over Time
Raw ranking data becomes valuable when you analyze trends over time. Look for patterns in your data: do rankings dip on certain days? Do they improve after content updates? Does a particular region consistently rank lower?
Build simple reporting functions that summarize your ranking history:
function generateWeeklyReport(tracker, keywords, countries) {
const report = [];
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
for (const keyword of keywords) {
for (const country of countries) {
const history = tracker.getHistory(keyword, country)
.filter(h => new Date(h.timestamp) >= oneWeekAgo);
if (history.length === 0) continue;
const positions = history
.filter(h => h.position !== null)
.map(h => h.position);
report.push({
keyword,
country,
avgPosition: positions.length > 0
? (positions.reduce((a, b) => a + b, 0) / positions.length).toFixed(1)
: 'N/A',
bestPosition: positions.length > 0 ? Math.min(...positions) : 'N/A',
worstPosition: positions.length > 0 ? Math.max(...positions) : 'N/A',
dataPoints: history.length
});
}
}
return report;
}
This weekly report gives you a clear picture of average rankings, best and worst positions, and data reliability for each keyword-region combination. Over time, these reports reveal whether your local SEO efforts are working and where you need to focus attention.
Ready to get started?
Sign up for Serpent API and get 100 free searches. No credit card required.
Try for FreeExplore: SERP API · News API · Image Search API · Try in Playground