VendorStacks
← All posts
Engineering7 min read

Tech Stack Lookup API: Building Real-Time Vendor Detection Into Your Application

A technical guide to implementing live tech stack detection using the VendorStacks API, with code examples for both cached lookups and on-demand scanning.

Stax
VendorStacks research desk

Why Real-Time Tech Stack Detection Matters

Most technographic data providers work from static databases refreshed weekly or monthly. This creates a fundamental problem: by the time you query for a company's tech stack, the data may already be stale. A competitor analysis tool showing last month's vendors misses recent migrations. A sales qualification system using week-old data routes leads to the wrong team.

The VendorStacks API solves this with a dual-mode architecture: instant lookups for previously indexed companies (sub-second response times) and on-demand live scanning for any domain (15-90 seconds). This guide shows how to implement both patterns in production applications.

Understanding the Two Lookup Modes

When you call GET /v1/check?url=example.com, the API first checks if we've recently scanned that domain. If we have indexed data, you get an instant response. If not, the API triggers a live scan that analyzes the company's public web presence in real-time.

This architecture lets you build applications that always work with current data without maintaining your own scanning infrastructure. You don't need to guess which companies to pre-cache or manage refresh schedules.

Implementation: Basic Tech Stack Lookup

Here's a production-ready implementation that handles both indexed and unindexed lookups:

import requests
import time
from typing import Dict, List, Optional

class VendorStacksClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.vendorstacks.com"
        self.headers = {"Authorization": f"Bearer {api_key}"}
    
    def get_tech_stack(self, domain: str, timeout: int = 120) -> Dict:
        """
        Get tech stack for a domain. Handles both instant and scanned lookups.
        
        Args:
            domain: Company domain (e.g., 'stripe.com')
            timeout: Maximum seconds to wait for live scan
        
        Returns:
            Dict with vendor_stack, evidence, and metadata
        """
        url = f"{self.base_url}/v1/check"
        params = {"url": domain}
        
        start_time = time.time()
        response = requests.get(url, params=params, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        
        elapsed = time.time() - start_time
        
        # Log whether this was instant (indexed) or scanned
        if elapsed < 2:
            print(f"Instant lookup for {domain} ({elapsed:.2f}s)")
        else:
            print(f"Live scan for {domain} ({elapsed:.2f}s)")
        
        return data
    
    def get_vendors_by_category(self, domain: str) -> Dict[str, List[str]]:
        """
        Get vendors organized by category.
        """
        data = self.get_tech_stack(domain)
        
        if not data.get("found", False):
            return {}
        
        return data.get("vendor_stack", {})

# Usage
client = VendorStacksClient("vr_live_your_key_here")

# Fast lookup for indexed company
stack = client.get_tech_stack("stripe.com")
print(f"Credits used: {stack['credits_used']}")
print(f"Credits remaining: {stack['credit_balance']}")

# Live scan for unindexed company (takes 15-90s)
stack = client.get_tech_stack("newstartup.com")

Handling Live Scans in User-Facing Applications

Live scans take 15-90 seconds, which is too long for synchronous HTTP requests in most UX patterns. Here's how to handle this correctly:

Pattern 1: Async Job Queue

from celery import Celery
import redis

app = Celery('vendor_scanner')
redis_client = redis.Redis()

@app.task
def scan_company_stack(domain: str, user_id: str):
    """
    Background task for tech stack scanning.
    """
    client = VendorStacksClient("vr_live_your_key_here")
    
    try:
        result = client.get_tech_stack(domain)
        
        # Cache the result
        cache_key = f"tech_stack:{domain}"
        redis_client.setex(
            cache_key,
            86400,  # 24 hour cache
            json.dumps(result)
        )
        
        # Notify user (webhook, websocket, email, etc.)
        notify_scan_complete(user_id, domain, result)
        
    except Exception as e:
        notify_scan_failed(user_id, domain, str(e))

# API endpoint
@app.route('/api/scan', methods=['POST'])
def start_scan():
    domain = request.json['domain']
    user_id = request.user.id
    
    # Check cache first
    cached = redis_client.get(f"tech_stack:{domain}")
    if cached:
        return jsonify(json.loads(cached))
    
    # Start background scan
    task = scan_company_stack.delay(domain, user_id)
    
    return jsonify({
        "status": "scanning",
        "task_id": task.id,
        "estimated_time": 60
    }), 202

Pattern 2: Optimistic Fetching

For applications that process companies in batches (lead enrichment, market research), fetch indexed data immediately and queue unindexed domains:

def enrich_company_list(domains: List[str]) -> Dict[str, Dict]:
    """
    Enrich a list of companies, separating instant vs. queued lookups.
    """
    client = VendorStacksClient("vr_live_your_key_here")
    results = {}
    to_scan = []
    
    for domain in domains:
        # Try /v1/company/{domain} first (never scans, instant)
        response = requests.get(
            f"https://api.vendorstacks.com/v1/company/{domain}",
            headers={"Authorization": f"Bearer {client.api_key}"}
        )
        
        if response.status_code == 200:
            results[domain] = response.json()
        else:
            # Not indexed - queue for scanning
            to_scan.append(domain)
    
    # Process unindexed domains in background
    for domain in to_scan:
        scan_company_stack.delay(domain, "batch_job_123")
    
    return results, to_scan

Cost-Efficient Implementation Patterns

VendorStacks charges 1 credit per successful lookup. Zero credits for failed scans or companies with no public vendor evidence. Here's how to minimize costs:

Cache Aggressively

def get_stack_with_cache(domain: str, max_age_hours: int = 24) -> Dict:
    """
    Check cache before hitting API.
    """
    cache_key = f"tech_stack:{domain}"
    cached = redis_client.get(cache_key)
    
    if cached:
        data = json.loads(cached)
        cached_at = data.get("scanned_at")
        
        if cached_at:
            age_hours = (time.time() - cached_at) / 3600
            if age_hours < max_age_hours:
                return data
    
    # Cache miss or stale - fetch fresh
    client = VendorStacksClient("vr_live_your_key_here")
    result = client.get_tech_stack(domain)
    
    # Only cache successful results
    if result.get("found", False):
        redis_client.setex(cache_key, max_age_hours * 3600, json.dumps(result))
    
    return result

Batch Processing with Rate Awareness

def process_domains_efficiently(domains: List[str]):
    """
    Process many domains while respecting credit balance.
    """
    client = VendorStacksClient("vr_live_your_key_here")
    
    # Check balance first
    balance_response = requests.get(
        "https://api.vendorstacks.com/v1/balance",
        headers=client.headers
    )
    credits_available = balance_response.json()["credit_balance"]
    
    print(f"Starting batch with {credits_available} credits")
    
    processed = 0
    for domain in domains:
        if credits_available < 10:  # Reserve buffer
            print(f"Low credits ({credits_available}). Stopping batch.")
            break
        
        result = client.get_tech_stack(domain)
        credits_used = result.get("credits_used", 0)
        credits_available = result.get("credit_balance", credits_available)
        
        processed += 1
        
        if credits_used > 0:
            print(f"Processed {domain}: {credits_used} credits")
        else:
            print(f"No vendors found for {domain}: 0 credits")
    
    return processed

Using Evidence Fields for Transparency

Every vendor in the response includes an evidence field showing exactly where VendorStacks found that vendor:

def analyze_vendor_evidence(domain: str):
    """
    Show how vendors were detected.
    """
    client = VendorStacksClient("vr_live_your_key_here")
    result = client.get_tech_stack(domain)
    
    if not result.get("found"):
        print(f"No public vendor evidence found for {domain}")
        return
    
    for category, vendors in result["vendor_stack"].items():
        for vendor in vendors:
            evidence_key = f"{vendor.lower().replace(' ', '_')}_evidence"
            evidence = result.get(evidence_key, "No evidence field")
            
            print(f"\n{vendor} ({category}):")
            print(f"  Source: {evidence}")
            print(f"  Confidence: {result.get('vendor_confidence', {}).get(vendor, 'N/A')}")

# Example output:
# Stripe (payments):
#   Source: https://example.com/legal/subprocessors - "Stripe, Inc. Payment processing"
#   Confidence: high

This transparency is critical for applications where users need to verify findings or understand detection methodology.

Real-World Performance Characteristics

Based on our index of 1,000 companies covering 324 distinct vendors:

  • Indexed lookups: <1 second response time
  • Live scans: 15-90 seconds depending on site complexity
  • Coverage: Our index includes the most-used vendors across 24 categories. The top vendors by company count are AWS (339 companies), Google Analytics (292), Stripe (270), OpenAI (210), and Anthropic (171)
  • Cost: Remember that failed scans and empty results cost 0 credits. You only pay for successful vendor detection.

Integrating Tech Stack Data Into Existing Systems

The most common integration pattern is enriching domain records as they enter your system:

@app.route('/webhook/new-lead', methods=['POST'])
def handle_new_lead():
    """
    Enrich incoming leads with tech stack data.
    """
    lead_data = request.json
    domain = lead_data.get('company_domain')
    
    if domain:
        # Non-blocking enrichment
        enrich_lead_tech_stack.delay(lead_data['id'], domain)
    
    return jsonify({"status": "accepted"})

@app.task
def enrich_lead_tech_stack(lead_id: str, domain: str):
    client = VendorStacksClient("vr_live_your_key_here")
    
    try:
        stack = client.get_tech_stack(domain)
        
        if stack.get("found"):
            # Update CRM with vendor data
            vendors_list = []
            for category, vendors in stack["vendor_stack"].items():
                vendors_list.extend(vendors)
            
            crm_update = {
                "tech_stack_vendors": vendors_list,
                "tech_stack_updated_at": time.time(),
                "uses_stripe": "Stripe" in vendors_list,
                "uses_openai": "OpenAI" in vendors_list,
                # Add flags for key vendors you care about
            }
            
            update_crm_record(lead_id, crm_update)
    
    except Exception as e:
        log_enrichment_error(lead_id, domain, e)

Building Reliable Systems Around Live Scanning

Live scanning introduces latency and potential failure modes. Here's how to handle them:

from tenacity import retry, stop_after_attempt, wait_exponential

class RobustVendorStacksClient(VendorStacksClient):
    
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=4, max=10)
    )
    def get_tech_stack_with_retry(self, domain: str) -> Optional[Dict]:
        """
        Retry failed scans with exponential backoff.
        """
        try:
            return self.get_tech_stack(domain)
        except requests.exceptions.Timeout:
            # Live scan took too long - may succeed on retry
            raise
        except requests.exceptions.HTTPError as e:
            if e.response.status_code >= 500:
                # Server error - retry
                raise
            else:
                # Client error (bad domain, etc.) - don't retry
                return None
    
    def get_tech_stack_safe(self, domain: str) -> Dict:
        """
        Never throws - returns empty dict on any failure.
        """
        try:
            result = self.get_tech_stack_with_retry(domain)
            return result if result else {"found": False}
        except Exception as e:
            print(f"Failed to get tech stack for {domain}: {e}")
            return {"found": False, "error": str(e)}

When to Use /v1/check vs /v1/company

The API provides two endpoints for single-domain lookups:

  • GET /v1/check?url={domain}: Returns indexed data instantly, triggers live scan if not indexed
  • GET /v1/company/{domain}: Returns indexed data only, never scans (returns 404 if not indexed)

Use /v1/company/{domain} when:

  • You're doing bulk lookups and will handle unindexed domains separately
  • You want to avoid triggering expensive scans
  • You're checking if we have data before deciding whether to scan

Use /v1/check?url={domain} when:

  • You always need current data and can wait for scans
  • You're processing individual domains in real-time
  • You want the simplest possible implementation

Conclusion

The VendorStacks API's dual-mode architecture gives you the flexibility to build both instant-response and always-current tech stack detection into your applications. By understanding the tradeoffs between indexed lookups and live scanning, implementing proper caching, and handling the latency of on-demand scans correctly, you can build reliable systems that work with fresh technographic data without managing scanning infrastructure yourself.

The key is matching the lookup pattern to your use case: instant lookups for interactive UX, background jobs for batch processing, and careful caching to minimize costs while maintaining data freshness.

About the author

Stax is the pangolin who fronts the VendorStacks research desk — a fitting mascot for a company that reads layered stacks for a living. Posts under this byline are written by the VendorStacks team.

VendorStacks is the subprocessor disclosure data API — structured vendor stacks with quoted evidence, 25 free credits to start.

Get an API key