Keyword gap analysis reveals opportunities where competitors rank but you don’t. Manual analysis is time-consuming and incomplete. This guide shows how to build an automated system using SERP API to continuously discover keyword gaps and prioritize SEO efforts for maximum impact.
Quick Links: SEO Rank Tracker Guide | Content Research Automation | API Playground
Understanding Keyword Gap Analysis
What is Keyword Gap Analysis?
Definition: Identifying keywords where competitors rank in top positions while your site doesn’t rank or ranks lower.
Why It Matters:
- Reveals proven keyword opportunities
- Shows what’s working for competitors
- Prioritizes SEO efforts
- Finds quick win opportunities
- Validates content strategy
Traditional vs. Automated Approach
| Aspect | Manual | Automated |
|---|---|---|
| Coverage | 20-50 keywords | Unlimited |
| Frequency | Quarterly | Daily/Weekly |
| Time Required | Days | Minutes |
| Accuracy | Subjective | Data-driven |
| Cost | High labor | Low automation |
Gap Analysis Framework
Analysis Dimensions
1. Keyword Discovery
├─ Competitor Keywords
├─ SERP Analysis
└─ Search Volumes
2. Gap Identification
├─ Missing Keywords
├─ Low Ranking Keywords
└─ Content Gaps
3. Opportunity Scoring
├─ Search Volume
├─ Competition Level
├─ Relevance
└─ Business Value
4. Strategy Development
├─ Content Planning
├─ Optimization Priorities
└─ Resource Allocation
Opportunity Types
Type 1: Complete Gaps
- Competitor ranks, you don’t
- Highest opportunity potential
- Priority: Immediate action
Type 2: Ranking Gaps
- Both rank, but competitor higher
- Medium opportunity
- Priority: Optimization
Type 3: Content Gaps
- Missing content angles
- Strategic opportunity
- Priority: Content creation
Technical Implementation
Step 1: Competitor Keyword Discovery
import requests
from typing import List, Dict, Set
from collections import defaultdict
import logging
class CompetitorKeywordDiscovery:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://www.searchcans.com/api/search"
self.logger = logging.getLogger(__name__)
def discover_competitor_keywords(self,
competitor_domain: str,
seed_keywords: List[str]) -> Dict:
"""Discover keywords where competitor ranks"""
competitor_keywords = defaultdict(list)
for keyword in seed_keywords:
ranking_data = self._check_keyword_ranking(
keyword,
competitor_domain
)
if ranking_data:
competitor_keywords[keyword].append(ranking_data)
# Find related keywords from SERP features
related_keywords = self._find_related_keywords(seed_keywords)
return {
'primary_keywords': dict(competitor_keywords),
'related_keywords': related_keywords,
'total_keywords': len(competitor_keywords) + len(related_keywords)
}
def _check_keyword_ranking(self,
keyword: str,
domain: str) -> Dict:
"""Check if and where domain ranks for keyword"""
params = {
'q': keyword,
'num': 100, # Check top 100
'market': 'US'
}
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
try:
response = requests.get(
self.base_url,
params=params,
headers=headers,
timeout=10
)
if response.status_code != 200:
return None
serp_data = response.json()
# Find domain in results
for idx, result in enumerate(serp_data.get('organic', []), 1):
url = result.get('link', '')
if domain in url:
return {
'keyword': keyword,
'position': idx,
'url': url,
'title': result.get('title', ''),
'snippet': result.get('snippet', '')
}
except Exception as e:
self.logger.error(f"Error checking {keyword}: {e}")
return None
def _find_related_keywords(self,
seed_keywords: List[str]) -> List[str]:
"""Find related keywords from SERP features"""
related = set()
for keyword in seed_keywords[:5]: # Limit to avoid too many calls
params = {
'q': keyword,
'num': 10,
'market': 'US'
}
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
try:
response = requests.get(
self.base_url,
params=params,
headers=headers,
timeout=10
)
if response.status_code == 200:
serp_data = response.json()
# Extract from related searches
if 'related_searches' in serp_data:
for item in serp_data['related_searches']:
related.add(item.get('query', ''))
# Extract from People Also Ask
if 'people_also_ask' in serp_data:
for item in serp_data['people_also_ask']:
question = item.get('question', '')
# Extract keywords from question
# Simplified - in production use NLP
words = question.lower().split()
if len(words) > 2:
related.add(' '.join(words[:4]))
except Exception as e:
self.logger.error(f"Error finding related for {keyword}: {e}")
return list(related)
Step 2: Your Site Ranking Analysis
class SiteRankingAnalyzer:
def __init__(self, api_key: str, your_domain: str):
self.api_key = api_key
self.your_domain = your_domain
self.base_url = "https://www.searchcans.com/api/search"
def analyze_your_rankings(self,
keywords: List[str]) -> Dict:
"""Analyze your site's rankings for keywords"""
rankings = {}
for keyword in keywords:
rank_data = self._get_ranking(keyword)
if rank_data:
rankings[keyword] = rank_data
else:
rankings[keyword] = {
'position': None,
'url': None,
'status': 'not_ranking'
}
return rankings
def _get_ranking(self, keyword: str) -> Dict:
"""Get your ranking for a keyword"""
params = {
'q': keyword,
'num': 100,
'market': 'US'
}
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
try:
response = requests.get(
self.base_url,
params=params,
headers=headers,
timeout=10
)
if response.status_code != 200:
return None
serp_data = response.json()
# Find your domain
for idx, result in enumerate(serp_data.get('organic', []), 1):
url = result.get('link', '')
if self.your_domain in url:
return {
'position': idx,
'url': url,
'title': result.get('title', ''),
'status': 'ranking'
}
except Exception as e:
logging.error(f"Error getting ranking for {keyword}: {e}")
return None
Step 3: Gap Identification and Scoring
from typing import Tuple
class KeywordGapAnalyzer:
def __init__(self):
self.gap_types = {
'complete_gap': 100, # Not ranking at all
'large_gap': 75, # >20 positions behind
'medium_gap': 50, # 11-20 positions behind
'small_gap': 25 # 1-10 positions behind
}
def identify_gaps(self,
competitor_rankings: Dict,
your_rankings: Dict) -> List[Dict]:
"""Identify keyword gaps"""
gaps = []
for keyword, comp_data in competitor_rankings.items():
if not comp_data:
continue
your_data = your_rankings.get(keyword, {})
# Calculate gap
gap_analysis = self._analyze_gap(
keyword,
comp_data,
your_data
)
if gap_analysis:
gaps.append(gap_analysis)
# Sort by opportunity score
gaps.sort(key=lambda x: x['opportunity_score'], reverse=True)
return gaps
def _analyze_gap(self,
keyword: str,
competitor: Dict,
yours: Dict) -> Dict:
"""Analyze individual keyword gap"""
comp_position = competitor.get('position', 100)
your_position = yours.get('position')
# Determine gap type
if your_position is None:
gap_type = 'complete_gap'
position_difference = 100 # Not ranking
else:
position_difference = your_position - comp_position
if position_difference > 20:
gap_type = 'large_gap'
elif position_difference > 10:
gap_type = 'medium_gap'
elif position_difference > 0:
gap_type = 'small_gap'
else:
return None # You rank higher, no gap
# Calculate opportunity score
opportunity_score = self._calculate_opportunity_score(
gap_type,
comp_position,
keyword
)
return {
'keyword': keyword,
'gap_type': gap_type,
'competitor_position': comp_position,
'your_position': your_position,
'position_difference': position_difference,
'opportunity_score': opportunity_score,
'competitor_url': competitor.get('url'),
'competitor_title': competitor.get('title')
}
def _calculate_opportunity_score(self,
gap_type: str,
competitor_position: int,
keyword: str) -> float:
"""Calculate opportunity score (0-100)"""
# Base score from gap type
base_score = self.gap_types.get(gap_type, 0)
# Bonus for competitor ranking in top 3
position_bonus = 0
if competitor_position <= 3:
position_bonus = 20
elif competitor_position <= 10:
position_bonus = 10
# Keyword length bonus (longer = more specific = easier)
length_bonus = min(len(keyword.split()) * 5, 15)
# Calculate final score
total_score = base_score + position_bonus + length_bonus
return min(total_score, 100)
Step 4: Content Gap Analysis
class ContentGapAnalyzer:
def __init__(self, api_key: str):
self.api_key = api_key
def analyze_content_gaps(self,
keyword: str,
your_url: str,
competitor_url: str) -> Dict:
"""Analyze content differences"""
# In production, use Reader API API to get full content
# For this example, we'll analyze SERP snippets
params = {
'q': keyword,
'num': 10,
'market': 'US'
}
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
try:
response = requests.get(
"https://www.searchcans.com/api/search",
params=params,
headers=headers,
timeout=10
)
if response.status_code != 200:
return None
serp_data = response.json()
# Find both URLs in results
your_result = None
competitor_result = None
for result in serp_data.get('organic', []):
url = result.get('link', '')
if your_url in url:
your_result = result
elif competitor_url in url:
competitor_result = result
if not competitor_result:
return None
# Analyze differences
gaps = {
'title_analysis': self._analyze_titles(
your_result.get('title', '') if your_result else '',
competitor_result.get('title', '')
),
'snippet_analysis': self._analyze_snippets(
your_result.get('snippet', '') if your_result else '',
competitor_result.get('snippet', '')
),
'serp_features': self._check_serp_features(
serp_data,
competitor_url
)
}
return gaps
except Exception as e:
logging.error(f"Content gap analysis error: {e}")
return None
def _analyze_titles(self, your_title: str, comp_title: str) -> Dict:
"""Compare title strategies"""
your_words = set(your_title.lower().split())
comp_words = set(comp_title.lower().split())
missing_words = comp_words - your_words
return {
'your_length': len(your_title),
'competitor_length': len(comp_title),
'missing_terms': list(missing_words),
'has_numbers': any(char.isdigit() for char in comp_title),
'has_year': '2024' in comp_title or '2025' in comp_title
}
def _analyze_snippets(self, your_snippet: str, comp_snippet: str) -> Dict:
"""Compare snippet content"""
# Extract key phrases (simplified)
import re
your_phrases = set(re.findall(r'\b\w+\s+\w+\b', your_snippet.lower()))
comp_phrases = set(re.findall(r'\b\w+\s+\w+\b', comp_snippet.lower()))
unique_to_competitor = comp_phrases - your_phrases
return {
'your_length': len(your_snippet),
'competitor_length': len(comp_snippet),
'unique_competitor_phrases': list(unique_to_competitor)[:10],
'content_angle': self._identify_content_angle(comp_snippet)
}
def _identify_content_angle(self, snippet: str) -> str:
"""Identify content type from snippet"""
snippet_lower = snippet.lower()
if any(word in snippet_lower for word in ['how to', 'guide', 'tutorial']):
return 'educational'
elif any(word in snippet_lower for word in ['best', 'top', 'review']):
return 'comparison'
elif any(word in snippet_lower for word in ['what is', 'definition']):
return 'informational'
else:
return 'general'
def _check_serp_features(self, serp_data: Dict, competitor_url: str) -> Dict:
"""Check which SERP features competitor owns"""
features = {
'featured_snippet': False,
'people_also_ask': False,
'video': False
}
# Check featured snippet
if 'featured_snippet' in serp_data:
snippet_url = serp_data['featured_snippet'].get('link', '')
if competitor_url in snippet_url:
features['featured_snippet'] = True
# Check PAA
if 'people_also_ask' in serp_data:
for paa in serp_data['people_also_ask']:
paa_url = paa.get('link', '')
if competitor_url in paa_url:
features['people_also_ask'] = True
break
return features
Step 5: Complete Pipeline
class KeywordGapPipeline:
def __init__(self, api_key: str, your_domain: str):
self.discoverer = CompetitorKeywordDiscovery(api_key)
self.analyzer = SiteRankingAnalyzer(api_key, your_domain)
self.gap_analyzer = KeywordGapAnalyzer()
self.content_analyzer = ContentGapAnalyzer(api_key)
def run_gap_analysis(self,
competitor_domains: List[str],
seed_keywords: List[str]) -> Dict:
"""Run complete keyword gap analysis"""
print(f"Starting gap analysis for {len(competitor_domains)} competitors...")
all_gaps = []
for competitor in competitor_domains:
print(f"\nAnalyzing {competitor}...")
# 1. Discover competitor keywords
comp_keywords = self.discoverer.discover_competitor_keywords(
competitor,
seed_keywords
)
# Get all keywords (primary + related)
all_keywords = list(comp_keywords['primary_keywords'].keys())
all_keywords.extend(comp_keywords['related_keywords'])
print(f"Found {len(all_keywords)} keywords for {competitor}")
# 2. Analyze your rankings
your_rankings = self.analyzer.analyze_your_rankings(all_keywords)
# 3. Identify gaps
gaps = self.gap_analyzer.identify_gaps(
comp_keywords['primary_keywords'],
your_rankings
)
# Add competitor info
for gap in gaps:
gap['competitor_domain'] = competitor
all_gaps.extend(gaps)
# Sort all gaps by opportunity score
all_gaps.sort(key=lambda x: x['opportunity_score'], reverse=True)
# Generate insights
insights = self._generate_insights(all_gaps)
return {
'total_gaps': len(all_gaps),
'top_opportunities': all_gaps[:20],
'gaps_by_type': self._group_by_type(all_gaps),
'insights': insights
}
def _group_by_type(self, gaps: List[Dict]) -> Dict:
"""Group gaps by type"""
grouped = defaultdict(list)
for gap in gaps:
grouped[gap['gap_type']].append(gap)
return {
gap_type: len(gaps)
for gap_type, gaps in grouped.items()
}
def _generate_insights(self, gaps: List[Dict]) -> Dict:
"""Generate actionable insights"""
if not gaps:
return {}
# Calculate averages
avg_score = sum(g['opportunity_score'] for g in gaps) / len(gaps)
# Find patterns
gap_types = defaultdict(int)
for gap in gaps:
gap_types[gap['gap_type']] += 1
# Top priority keywords
top_priority = [
gap['keyword']
for gap in gaps[:10]
]
return {
'avg_opportunity_score': round(avg_score, 2),
'total_complete_gaps': gap_types.get('complete_gap', 0),
'quick_wins': gap_types.get('small_gap', 0),
'top_priority_keywords': top_priority,
'recommendation': self._get_recommendation(gap_types)
}
def _get_recommendation(self, gap_types: Dict) -> str:
"""Generate strategic recommendation"""
complete_gaps = gap_types.get('complete_gap', 0)
small_gaps = gap_types.get('small_gap', 0)
if complete_gaps > small_gaps:
return "Focus on creating new content for complete gaps"
else:
return "Prioritize optimizing existing content for quick wins"
Practical Example: SaaS Company Analysis
Scenario
A project management SaaS wants to identify keyword gaps against top 3 competitors.
Implementation
# Initialize pipeline
pipeline = KeywordGapPipeline(
api_key='your_api_key',
your_domain='yourproduct.com'
)
# Define competitors
competitors = [
'asana.com',
'monday.com',
'trello.com'
]
# Seed keywords
seed_keywords = [
'project management software',
'team collaboration tools',
'task management app',
'agile project management',
'project planning software',
# ... more keywords
]
# Run analysis
results = pipeline.run_gap_analysis(competitors, seed_keywords)
# Display results
print(f"\n{'='*60}")
print("KEYWORD GAP ANALYSIS RESULTS")
print(f"{'='*60}\n")
print(f"Total Gaps Found: {results['total_gaps']}")
print(f"\nGaps by Type:")
for gap_type, count in results['gaps_by_type'].items():
print(f" - {gap_type}: {count}")
print(f"\nTop 10 Opportunities:")
for idx, gap in enumerate(results['top_opportunities'][:10], 1):
print(f"\n{idx}. {gap['keyword']}")
print(f" Score: {gap['opportunity_score']}/100")
print(f" Gap Type: {gap['gap_type']}")
print(f" Competitor: {gap['competitor_domain']} (Rank #{gap['competitor_position']})")
print(f"\n{'='*60}")
print("INSIGHTS")
print(f"{'='*60}")
print(f"Avg Opportunity Score: {results['insights']['avg_opportunity_score']}")
print(f"Complete Gaps: {results['insights']['total_complete_gaps']}")
print(f"Quick Wins Available: {results['insights']['quick_wins']}")
print(f"\nRecommendation: {results['insights']['recommendation']}")
Results
After analyzing 3 competitors with 50 seed keywords:
Total Gaps
147 keywords
Complete Gaps
68 (new content needed)
Small Gaps
34 (optimization opportunities)
Topic Cluster Strategy
Topic Cluster Strategy: “agile sprint planning” (Score: 95/100)
Action Plan Generated:
- Create content for top 20 complete gaps (Week 1-4)
- Optimize existing pages for small gaps (Week 5-6)
- Target featured snippets for 10 high-value keywords (Week 7-8)
Cost Analysis
Monthly Analysis (3 competitors, 50 seed keywords):
- Initial discovery: 50 × 3 = 150 calls
- Related keywords: ~100 calls
- Ranking checks: 50 × 2 (yours + verification) = 100 calls
- Monthly total: ~350 calls
SearchCans Cost:
- Starter Plan: $29/month (50,000 calls)
- Usage: 0.7% of quota
- Cost per analysis: ~$29
Manual Alternative:
- Research time: 40 hours
- Labor cost: $2,000+
- Cost savings: 98.5%
View pricing details.
Best Practices
1. Regular Monitoring
Set up weekly or monthly automated gap analysis to catch new opportunities early.
2. Prioritization Framework
def prioritize_gaps(gaps: List[Dict]) -> List[Dict]:
"""Prioritize based on multiple factors"""
for gap in gaps:
# Calculate priority score
score = gap['opportunity_score']
# Boost for business-critical keywords
if is_business_critical(gap['keyword']):
score *= 1.5
# Boost for quick wins
if gap['gap_type'] == 'small_gap':
score *= 1.2
gap['priority_score'] = score
return sorted(gaps, key=lambda x: x['priority_score'], reverse=True)
3. Content Strategy Integration
Use gap analysis to inform:
- Editorial calendar
- Content briefs
- Optimization priorities
- Resource allocation
Related Resources
Technical Guides:
- SEO Rank Tracker Development - Tracking system
- Content Research Automation - Content strategy
- API Documentation - Complete reference
Get Started:
- Free Registration - 100 credits included
- View Pricing - Affordable plans
- API Playground - Test integration
SEO Resources:
- Python SEO Automation - Code examples
- Best Practices - Optimization tips
SearchCans provides cost-effective SERP API services optimized for SEO research, keyword analysis, and competitive intelligence. [Start your free trial →](/register/]