SearchCans

SERP API Keyword Gap Analysis: Automate Competitor Research & Find Opportunities

Automate keyword gap analysis with SERP API. Discover untapped opportunities. Python code, competitor analysis, step-by-step guide. Find high-value keywords competitors rank for.

4 min read

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

AspectManualAutomated
Coverage20-50 keywordsUnlimited
FrequencyQuarterlyDaily/Weekly
Time RequiredDaysMinutes
AccuracySubjectiveData-driven
CostHigh laborLow 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:

  1. Create content for top 20 complete gaps (Week 1-4)
  2. Optimize existing pages for small gaps (Week 5-6)
  3. 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

Technical Guides:

Get Started:

SEO Resources:


SearchCans provides cost-effective SERP API services optimized for SEO research, keyword analysis, and competitive intelligence. [Start your free trial →](/register/]

Jessica Wang

Jessica Wang

SEO Platform Engineer

Remote, US

SEO engineer with expertise in building large-scale SEO monitoring systems. Previously built SEO platforms serving 10K+ customers.

SEOPlatform EngineeringData Analysis
View all →

Trending articles will be displayed here.

Ready to try SearchCans?

Get 100 free credits and start using our SERP API today. No credit card required.