Amazon frequently changes search result layouts, ad placements, and product card structures. Scrapers break when this happens. This tutorial monitors Amazon SERP structure via the Scavio API, detecting when result formats change, new elements appear, or product positions shift. Each check costs $0.005.
Prerequisites
- Python 3.8+
- requests library
- A Scavio API key from scavio.dev
- Amazon product categories to monitor
Walkthrough
Step 1: Capture Amazon SERP structure snapshots
Record the structure of Amazon search results including result types and positions.
import os, requests, json, hashlib
from datetime import datetime
API_KEY = os.environ['SCAVIO_API_KEY']
SH = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}
def snapshot_amazon_serp(query):
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': query, 'platform': 'amazon', 'country_code': 'us'}, timeout=10).json()
organic = data.get('organic_results', [])
structure = {
'query': query,
'timestamp': datetime.now().isoformat(),
'total_results': len(organic),
'result_types': [],
'has_sponsored': any('sponsored' in json.dumps(r).lower() for r in organic),
'has_ratings': sum(1 for r in organic if r.get('rating')),
'has_prices': sum(1 for r in organic if r.get('price') or r.get('extracted_price')),
'top_domains': list(set(r.get('displayed_link', '').split('/')[0] for r in organic[:5])),
}
for r in organic:
rtype = 'sponsored' if 'sponsored' in json.dumps(r).lower() else 'organic'
structure['result_types'].append(rtype)
content_hash = hashlib.md5(json.dumps(structure['result_types']).encode()).hexdigest()
structure['layout_hash'] = content_hash
return structure
QUERIES = ['wireless earbuds', 'protein powder', 'usb c hub']
for q in QUERIES:
snap = snapshot_amazon_serp(q)
sponsored = snap['result_types'].count('sponsored')
organic = snap['result_types'].count('organic')
print(f' {q:30} | Results: {snap["total_results"]} | Organic: {organic} | Sponsored: {sponsored} | Hash: {snap["layout_hash"][:8]}')
print(f'\nCost: ${len(QUERIES) * 0.005:.3f}')Step 2: Compare snapshots to detect layout changes
Diff daily snapshots to find structural changes in Amazon results.
HISTORY_FILE = 'amazon_layout_history.json'
def save_and_compare(snapshots):
try:
with open(HISTORY_FILE) as f:
history = json.load(f)
except FileNotFoundError:
history = []
today = {'date': datetime.now().strftime('%Y-%m-%d'), 'snapshots': snapshots}
changes = []
if history:
prev_snaps = {s['query']: s for s in history[-1]['snapshots']}
for snap in snapshots:
prev = prev_snaps.get(snap['query'])
if not prev:
continue
if snap['layout_hash'] != prev['layout_hash']:
changes.append({'query': snap['query'], 'type': 'layout_change',
'detail': f'Layout hash changed: {prev["layout_hash"][:8]} -> {snap["layout_hash"][:8]}'})
if snap['total_results'] != prev['total_results']:
changes.append({'query': snap['query'], 'type': 'result_count',
'detail': f'Results: {prev["total_results"]} -> {snap["total_results"]}'})
prev_sponsored = prev['result_types'].count('sponsored')
curr_sponsored = snap['result_types'].count('sponsored')
if prev_sponsored != curr_sponsored:
changes.append({'query': snap['query'], 'type': 'ad_change',
'detail': f'Sponsored: {prev_sponsored} -> {curr_sponsored}'})
history.append(today)
with open(HISTORY_FILE, 'w') as f:
json.dump(history, f, indent=2)
return changes
snapshots = [snapshot_amazon_serp(q) for q in QUERIES]
changes = save_and_compare(snapshots)
print(f'\nLayout changes detected: {len(changes)}')
for c in changes:
print(f' [{c["type"]:15}] {c["query"]:25} | {c["detail"]}')Step 3: Generate layout change report
Compile changes into an actionable report for e-commerce teams.
def layout_change_report(changes, snapshots):
print(f'\n{"=" * 60}')
print(f' Amazon Layout Change Report - {datetime.now().strftime("%Y-%m-%d")}')
print(f'{"=" * 60}')
print(f'\n Queries monitored: {len(snapshots)}')
print(f' Changes detected: {len(changes)}')
# Current structure summary
print(f'\n Current Layout Summary:')
for snap in snapshots:
sponsored = snap['result_types'].count('sponsored')
organic = snap['result_types'].count('organic')
print(f' {snap["query"]:25} | Organic: {organic} | Sponsored: {sponsored} | Prices: {snap["has_prices"]} | Ratings: {snap["has_ratings"]}')
if changes:
by_type = {}
for c in changes:
by_type.setdefault(c['type'], []).append(c)
for ctype, items in by_type.items():
print(f'\n {ctype.replace("_", " ").title()} ({len(items)}):')
for item in items:
print(f' {item["query"]}: {item["detail"]}')
else:
print(f'\n No layout changes since last scan.')
print(f'\n Daily cost: ${len(snapshots) * 0.005:.3f}')
print(f' No scrapers needed. No proxy rotation. No HTML parsing.')
layout_change_report(changes, snapshots)Python Example
import os, requests, json, hashlib
SH = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
def amazon_layout(query):
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': query, 'platform': 'amazon', 'country_code': 'us'}, timeout=10).json()
results = data.get('organic_results', [])
h = hashlib.md5(json.dumps([r.get('position') for r in results]).encode()).hexdigest()[:8]
print(f'{query[:30]:30} | {len(results)} results | hash: {h}')
amazon_layout('wireless earbuds')JavaScript Example
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
const data = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST', headers: SH,
body: JSON.stringify({ query: 'wireless earbuds', platform: 'amazon', country_code: 'us' })
}).then(r => r.json());
console.log(`Results: ${(data.organic_results || []).length}`);Expected Output
wireless earbuds | Results: 10 | Organic: 7 | Sponsored: 3 | Hash: 4f2a8b1c
protein powder | Results: 10 | Organic: 8 | Sponsored: 2 | Hash: 7d3e9f5a
usb c hub | Results: 10 | Organic: 8 | Sponsored: 2 | Hash: 1b6c4d8e
Cost: $0.015
Layout changes detected: 1
[ad_change ] wireless earbuds | Sponsored: 2 -> 3
============================================================
Amazon Layout Change Report - 2026-05-21
============================================================
Queries monitored: 3
Changes detected: 1
No scrapers needed. No proxy rotation. No HTML parsing.