Tutorial

How to Track Local Rankings Without UULE

Track local search rankings without UULE parameter hacks. Use a geo-targeted search API to get location-specific SERPs with a simple country and location field.

Tracking local search rankings without UULE parameter hacks is now possible through geo-targeted search APIs that handle location simulation server-side. UULE parameters -- the encoded location strings appended to Google search URLs -- are fragile, undocumented, and break when Google changes their format. The Scavio search API accepts a location field alongside your query, returning SERPs as they appear from that specific city or region without any client-side UULE encoding. This tutorial builds a local rank tracker that monitors keyword positions across multiple locations and detects rank differences between cities.

Prerequisites

  • Python 3.8 or higher installed
  • requests library installed (pip install requests)
  • A Scavio API key from scavio.dev
  • Target keywords and locations to track

Walkthrough

Step 1: Define keywords and target locations

Create a configuration mapping keywords to the locations you want to track. Each location is a city or region string that the API uses for geo-targeting. Include your own domain to track its position.

Python
TRACKING_CONFIG = {
    'keywords': [
        'plumber near me',
        'emergency plumbing service',
        'water heater repair'
    ],
    'locations': [
        'Austin, Texas, United States',
        'Dallas, Texas, United States',
        'Houston, Texas, United States',
        'San Antonio, Texas, United States'
    ],
    'my_domain': 'austinplumbingpros.com'
}

Step 2: Fetch geo-targeted search results

POST to the Scavio API with the location field set to the target city. The API returns SERPs as seen from that location, including local pack results, organic positions, and map listings.

Python
import requests
import os

API_KEY = os.environ.get('SCAVIO_API_KEY', 'your_scavio_api_key')

def fetch_local_serp(query: str, location: str) -> dict:
    response = requests.post(
        'https://api.scavio.dev/api/v1/search',
        headers={'x-api-key': API_KEY},
        json={
            'query': query,
            'country_code': 'us',
            'location': location
        }
    )
    response.raise_for_status()
    return response.json()

Step 3: Extract position for your domain

Search the organic results and local pack for your domain. Return the position if found, or None if your domain does not appear in the top results for that location.

Python
def find_domain_position(data: dict, domain: str) -> dict:
    result = {'organic_position': None, 'local_pack_position': None}
    for r in data.get('organic_results', []):
        if domain in r.get('link', ''):
            result['organic_position'] = r.get('position')
            break
    for i, r in enumerate(data.get('local_pack', []), 1):
        if domain in r.get('link', '') or domain in r.get('website', ''):
            result['local_pack_position'] = i
            break
    return result

Step 4: Run the tracker across all keyword-location pairs

Iterate all combinations of keywords and locations, fetch results, extract positions, and print a comparison table showing how rankings differ by city.

Python
from datetime import datetime
import json
from pathlib import Path

def run_local_tracker():
    results = []
    config = TRACKING_CONFIG
    print(f'Local Rank Tracker - {datetime.now().strftime("%Y-%m-%d %H:%M")}')
    print(f'{"Keyword":<30} {"Location":<35} {"Organic":<10} {"Local Pack"}')
    print('-' * 90)
    for keyword in config['keywords']:
        for location in config['locations']:
            data = fetch_local_serp(keyword, location)
            pos = find_domain_position(data, config['my_domain'])
            organic = str(pos['organic_position']) if pos['organic_position'] else '--'
            local_pack = str(pos['local_pack_position']) if pos['local_pack_position'] else '--'
            print(f'{keyword:<30} {location:<35} {organic:<10} {local_pack}')
            results.append({
                'keyword': keyword, 'location': location,
                'organic': pos['organic_position'],
                'local_pack': pos['local_pack_position'],
                'timestamp': datetime.now().isoformat()
            })
    Path('local_rankings.json').write_text(json.dumps(results, indent=2))
    print(f'\nSaved {len(results)} data points to local_rankings.json')

run_local_tracker()

Python Example

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

API_KEY = os.environ.get('SCAVIO_API_KEY', 'your_scavio_api_key')
ENDPOINT = 'https://api.scavio.dev/api/v1/search'

TRACKING_CONFIG = {
    'keywords': ['plumber near me', 'emergency plumbing service', 'water heater repair'],
    'locations': [
        'Austin, Texas, United States',
        'Dallas, Texas, United States',
        'Houston, Texas, United States',
        'San Antonio, Texas, United States'
    ],
    'my_domain': 'austinplumbingpros.com'
}

def fetch_local_serp(query: str, location: str) -> dict:
    response = requests.post(
        ENDPOINT,
        headers={'x-api-key': API_KEY},
        json={'query': query, 'country_code': 'us', 'location': location}
    )
    response.raise_for_status()
    return response.json()

def find_domain_position(data: dict, domain: str) -> dict:
    result = {'organic_position': None, 'local_pack_position': None}
    for r in data.get('organic_results', []):
        if domain in r.get('link', ''):
            result['organic_position'] = r.get('position')
            break
    for i, r in enumerate(data.get('local_pack', []), 1):
        if domain in r.get('link', '') or domain in r.get('website', ''):
            result['local_pack_position'] = i
            break
    return result

def run_local_tracker():
    config = TRACKING_CONFIG
    results = []
    print(f'Local Rank Tracker - {datetime.now().strftime("%Y-%m-%d %H:%M")}')
    print(f'{"Keyword":<30} {"Location":<35} {"Organic":<10} {"Local Pack"}')
    print('-' * 90)
    for keyword in config['keywords']:
        for location in config['locations']:
            data = fetch_local_serp(keyword, location)
            pos = find_domain_position(data, config['my_domain'])
            organic = str(pos['organic_position']) if pos['organic_position'] else '--'
            local_pack = str(pos['local_pack_position']) if pos['local_pack_position'] else '--'
            print(f'{keyword:<30} {location:<35} {organic:<10} {local_pack}')
            results.append({
                'keyword': keyword, 'location': location,
                'organic': pos['organic_position'],
                'local_pack': pos['local_pack_position'],
                'timestamp': datetime.now().isoformat()
            })
    Path('local_rankings.json').write_text(json.dumps(results, indent=2))
    print(f'\nSaved {len(results)} data points to local_rankings.json')

if __name__ == '__main__':
    run_local_tracker()

JavaScript Example

JavaScript
const API_KEY = process.env.SCAVIO_API_KEY || 'your_scavio_api_key';
const ENDPOINT = 'https://api.scavio.dev/api/v1/search';
const fs = require('fs');

const TRACKING_CONFIG = {
  keywords: ['plumber near me', 'emergency plumbing service', 'water heater repair'],
  locations: [
    'Austin, Texas, United States',
    'Dallas, Texas, United States',
    'Houston, Texas, United States',
    'San Antonio, Texas, United States'
  ],
  myDomain: 'austinplumbingpros.com'
};

async function fetchLocalSerp(query, location) {
  const response = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, country_code: 'us', location })
  });
  if (!response.ok) throw new Error('HTTP ' + response.status);
  return response.json();
}

function findDomainPosition(data, domain) {
  const result = { organicPosition: null, localPackPosition: null };
  for (const r of (data.organic_results || [])) {
    if ((r.link || '').includes(domain)) {
      result.organicPosition = r.position;
      break;
    }
  }
  (data.local_pack || []).forEach((r, i) => {
    if ((r.link || '').includes(domain) || (r.website || '').includes(domain)) {
      if (!result.localPackPosition) result.localPackPosition = i + 1;
    }
  });
  return result;
}

async function main() {
  const config = TRACKING_CONFIG;
  const results = [];
  console.log('Local Rank Tracker - ' + new Date().toISOString().slice(0, 16));
  for (const keyword of config.keywords) {
    for (const location of config.locations) {
      const data = await fetchLocalSerp(keyword, location);
      const pos = findDomainPosition(data, config.myDomain);
      const organic = pos.organicPosition !== null ? String(pos.organicPosition) : '--';
      const localPack = pos.localPackPosition !== null ? String(pos.localPackPosition) : '--';
      console.log(keyword.padEnd(30) + location.padEnd(35) + organic.padEnd(10) + localPack);
      results.push({ keyword, location, organic: pos.organicPosition, localPack: pos.localPackPosition, timestamp: new Date().toISOString() });
    }
  }
  fs.writeFileSync('local_rankings.json', JSON.stringify(results, null, 2));
  console.log('Saved ' + results.length + ' data points');
}

main().catch(console.error);

Expected Output

JSON
Local Rank Tracker - 2026-05-12 14:30
Keyword                        Location                            Organic    Local Pack
------------------------------------------------------------------------------------------
plumber near me                Austin, Texas, United States        3          1
plumber near me                Dallas, Texas, United States        --         --
plumber near me                Houston, Texas, United States       --         --
plumber near me                San Antonio, Texas, United States   12         --
emergency plumbing service     Austin, Texas, United States        2          1
emergency plumbing service     Dallas, Texas, United States        --         --

Saved 12 data points to local_rankings.json

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Python 3.8 or higher installed. requests library installed (pip install requests). A Scavio API key from scavio.dev. Target keywords and locations to track. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Track local search rankings without UULE parameter hacks. Use a geo-targeted search API to get location-specific SERPs with a simple country and location field.