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.
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 indexedGET /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.