SAM.gov API Complete Technical Guide
Comprehensive technical documentation for implementing the SAM.gov API, including authentication, code examples, error handling, and production deployment strategies.
Developer Reality Check: This guide shows you how to implement SAM.gov's API, but most developers switch to alternatives after seeing the complexity and limitations. Consider whether the 40+ hours of development time is worth it for your project.
Table of Contents
Authentication & API Keys
Step 1: Entity Registration (2-4 weeks)
Before accessing the SAM.gov API, you need to register your entity:
- Create SAM.gov account at
sam.gov
- Complete entity registration with UEI (formerly DUNS)
- Wait for entity validation (10-15 business days)
- Request "Data Entry" role for API access
- Wait for role approval (5-30 business days)
Step 2: Generate API Key
Once approved:
- Log into SAM.gov
- Navigate to "Data Services" → "API Information"
- Generate new API key
- Save key securely (it's only shown once)
# Environment variables for API key management
export SAM_API_KEY="your_32_character_api_key_here"
export SAM_API_BASE_URL="https://api.sam.gov"
# Test your API key
curl -H "X-Api-Key: $SAM_API_KEY" \
"https://api.sam.gov/opportunities/v2/search?limit=1"
API Endpoints & Parameters
Primary Endpoints
| Endpoint |
Purpose |
Rate Limit Impact |
/opportunities/v2/search |
Search contract opportunities |
1 request per search |
/opportunities/v2/{opportunityId} |
Get single opportunity |
1 request per opportunity |
/entity-information/v3/entities |
Entity lookup and validation |
1 request per entity |
/exclusions/v3/search |
Check exclusions database |
1 request per search |
Search Parameters for Opportunities
keyword - Search in title and description
postedFrom / postedTo - Date range (MM/DD/YYYY)
typeOfNoticeFilter - Notice type filter
typeOfSetAsideFilter - Set-aside type
stateFilter - State abbreviation
limit - Results per page (max 100)
offset - Pagination offset
Complete Code Examples
Python Implementation
import requests
import json
import time
from datetime import datetime, timedelta
import logging
class SAMAPIClient:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.sam.gov"
self.session = requests.Session()
self.session.headers.update({
'X-Api-Key': api_key,
'Accept': 'application/json'
})
self.rate_limit_remaining = 10 # Track daily limit
def search_opportunities(self, **params):
"""Search for opportunities with rate limiting"""
if self.rate_limit_remaining <= 0:
raise Exception("Daily rate limit exceeded (10 requests)")
endpoint = "/opportunities/v2/search"
url = f"{self.base_url}{endpoint}"
# Default parameters
default_params = {
'limit': 10, # Small limit due to rate constraints
'offset': 0,
'postedFrom': (datetime.now() - timedelta(days=30)).strftime('%m/%d/%Y')
}
default_params.update(params)
try:
response = self.session.get(url, params=default_params, timeout=30)
self.rate_limit_remaining -= 1
if response.status_code == 200:
data = response.json()
return self._process_opportunities(data)
elif response.status_code == 429:
raise Exception("Rate limit exceeded")
else:
response.raise_for_status()
except requests.exceptions.RequestException as e:
logging.error(f"SAM API Error: {e}")
raise
def _process_opportunities(self, data):
"""Process the complex SAM.gov response structure"""
opportunities = []
# SAM.gov returns nested structure
opp_data = data.get('opportunitiesData', [])
for opp in opp_data:
# Extract data from nested structure
processed_opp = {
'id': opp.get('noticeId'),
'title': opp.get('title'),
'agency': opp.get('fullParentPathName'),
'posted_date': opp.get('postedDate'),
'response_deadline': opp.get('responseDeadLine'),
'type': opp.get('type'),
'base_type': opp.get('baseType'),
# Contact info (often incomplete)
'contact': self._extract_contact_info(opp.get('pointOfContact', [])),
# Set-aside info
'set_aside': opp.get('typeOfSetAside'),
# Location info
'place_of_performance': opp.get('placeOfPerformance', {}),
# NAICS codes
'naics': [naics.get('code') for naics in opp.get('naicsCode', [])],
# Solicitation number
'solicitation_number': opp.get('solicitationNumber'),
# Award info (if available)
'award': self._extract_award_info(opp.get('award', {})),
# Important: Description is NOT available via API
'description': None, # Must scrape from SAM.gov website
# Raw data for debugging
'raw_data': opp
}
opportunities.append(processed_opp)
return {
'opportunities': opportunities,
'total_records': data.get('totalRecords', 0),
'rate_limit_remaining': self.rate_limit_remaining
}
def _extract_contact_info(self, contacts):
"""Extract contact information (often incomplete)"""
if not contacts:
return None
contact = contacts[0] # Take first contact
return {
'name': contact.get('fullName'),
'email': contact.get('email'),
'phone': contact.get('phone'),
'title': contact.get('title')
}
def _extract_award_info(self, award_data):
"""Extract award information if available"""
if not award_data:
return None
return {
'amount': award_data.get('amount'),
'date': award_data.get('date'),
'awardee': award_data.get('awardee', {}).get('name')
}
def get_opportunity_details(self, opportunity_id):
"""Get detailed information for a specific opportunity"""
if self.rate_limit_remaining <= 0:
raise Exception("Daily rate limit exceeded")
endpoint = f"/opportunities/v2/{opportunity_id}"
url = f"{self.base_url}{endpoint}"
try:
response = self.session.get(url, timeout=30)
self.rate_limit_remaining -= 1
if response.status_code == 200:
return response.json()
else:
response.raise_for_status()
except requests.exceptions.RequestException as e:
logging.error(f"SAM API Error: {e}")
raise
# Usage example
def main():
# Initialize client
api_key = "your_sam_gov_api_key"
client = SAMAPIClient(api_key)
try:
# Search for recent opportunities
result = client.search_opportunities(
keyword="software development",
limit=5 # Keep small due to rate limits
)
print(f"Found {len(result['opportunities'])} opportunities")
print(f"Rate limit remaining: {result['rate_limit_remaining']}")
for opp in result['opportunities']:
print(f"\nTitle: {opp['title']}")
print(f"Agency: {opp['agency']}")
print(f"Posted: {opp['posted_date']}")
print(f"Deadline: {opp['response_deadline']}")
print(f"NAICS: {', '.join(opp['naics']) if opp['naics'] else 'None'}")
print(f"Contact: {opp['contact']['email'] if opp['contact'] else 'None'}")
print(f"Description: NOT AVAILABLE VIA API") # Major limitation
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
JavaScript/Node.js Implementation
const axios = require('axios');
class SAMAPIClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseURL = 'https://api.sam.gov';
this.rateLimitRemaining = 10; // Track daily limit
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'X-Api-Key': apiKey,
'Accept': 'application/json'
},
timeout: 30000
});
}
async searchOpportunities(params = {}) {
if (this.rateLimitRemaining <= 0) {
throw new Error('Daily rate limit exceeded (10 requests)');
}
const defaultParams = {
limit: 10,
offset: 0,
postedFrom: this._getDateString(30) // Last 30 days
};
const searchParams = { ...defaultParams, ...params };
try {
const response = await this.client.get('/opportunities/v2/search', {
params: searchParams
});
this.rateLimitRemaining--;
return this._processOpportunities(response.data);
} catch (error) {
if (error.response?.status === 429) {
throw new Error('Rate limit exceeded');
}
throw new Error(`SAM API Error: ${error.message}`);
}
}
_processOpportunities(data) {
const opportunities = data.opportunitiesData?.map(opp => ({
id: opp.noticeId,
title: opp.title,
agency: opp.fullParentPathName,
postedDate: opp.postedDate,
responseDeadline: opp.responseDeadLine,
type: opp.type,
contact: this._extractContact(opp.pointOfContact),
setAside: opp.typeOfSetAside,
naics: opp.naicsCode?.map(n => n.code) || [],
solicitationNumber: opp.solicitationNumber,
// Description NOT available via API
description: null,
rawData: opp
})) || [];
return {
opportunities,
totalRecords: data.totalRecords || 0,
rateLimitRemaining: this.rateLimitRemaining
};
}
_extractContact(contacts) {
if (!contacts || contacts.length === 0) return null;
const contact = contacts[0];
return {
name: contact.fullName,
email: contact.email,
phone: contact.phone,
title: contact.title
};
}
_getDateString(daysAgo) {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
return date.toLocaleDateString('en-US');
}
}
// Usage example
async function main() {
const client = new SAMAPIClient('your_sam_gov_api_key');
try {
const result = await client.searchOpportunities({
keyword: 'software development',
limit: 5
});
console.log(`Found ${result.opportunities.length} opportunities`);
console.log(`Rate limit remaining: ${result.rateLimitRemaining}`);
result.opportunities.forEach(opp => {
console.log(`\nTitle: ${opp.title}`);
console.log(`Agency: ${opp.agency}`);
console.log(`Posted: ${opp.postedDate}`);
console.log(`Contact: ${opp.contact?.email || 'None'}`);
console.log(`Description: NOT AVAILABLE VIA API`);
});
} catch (error) {
console.error(`Error: ${error.message}`);
}
}
main();
Error Handling & Rate Limits
Common Error Responses
| Status Code |
Error |
Cause |
Solution |
| 401 |
Unauthorized |
Invalid API key |
Check key format and permissions |
| 403 |
Forbidden |
Role not approved |
Request Data Entry role in SAM.gov |
| 429 |
Too Many Requests |
Rate limit exceeded |
Wait until next day (daily reset) |
| 500 |
Internal Server Error |
SAM.gov system issue |
Retry later, contact SAM support |
# Python error handling example
import time
from requests.exceptions import RequestException, HTTPError, Timeout
def robust_sam_request(client, endpoint, params=None, max_retries=3):
"""Make SAM API request with comprehensive error handling"""
for attempt in range(max_retries):
try:
if client.rate_limit_remaining <= 0:
raise Exception("Rate limit exhausted - wait until tomorrow")
response = client.session.get(
f"{client.base_url}{endpoint}",
params=params,
timeout=30
)
if response.status_code == 200:
client.rate_limit_remaining -= 1
return response.json()
elif response.status_code == 401:
raise Exception("Invalid API key - check SAM.gov account")
elif response.status_code == 403:
raise Exception("Access denied - request Data Entry role")
elif response.status_code == 429:
raise Exception("Rate limit exceeded - daily limit is 10 requests")
elif response.status_code >= 500:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
print(f"Server error, retrying in {wait_time} seconds...")
time.sleep(wait_time)
continue
else:
raise Exception("SAM.gov server error - try again later")
else:
response.raise_for_status()
except Timeout:
if attempt < max_retries - 1:
print("Request timeout, retrying...")
time.sleep(2)
continue
else:
raise Exception("Request timeout - SAM.gov may be slow")
except RequestException as e:
raise Exception(f"Network error: {e}")
raise Exception("Max retries exceeded")
Production Deployment Considerations
Rate Limit Management
- Daily Budget: 10 requests for public, 1,000 for entity users
- Request Prioritization: Cache aggressively, batch operations
- Monitoring: Track remaining requests per day
- Fallback Strategy: Manual CSV downloads when limits hit
# Production rate limiting strategy
import redis
from datetime import datetime, timedelta
class ProductionSAMClient:
def __init__(self, api_key, redis_host='localhost'):
self.api_key = api_key
self.redis = redis.Redis(host=redis_host, decode_responses=True)
self.rate_limit_key = f"sam_api_calls_{datetime.now().strftime('%Y-%m-%d')}"
def can_make_request(self):
"""Check if we can make another API call today"""
daily_calls = int(self.redis.get(self.rate_limit_key) or 0)
return daily_calls < 10 # Adjust for your plan
def record_api_call(self):
"""Record that we made an API call"""
pipe = self.redis.pipeline()
pipe.incr(self.rate_limit_key)
pipe.expire(self.rate_limit_key, 86400) # 24 hour TTL
pipe.execute()
def get_cached_result(self, cache_key):
"""Get cached result to avoid unnecessary API calls"""
cached = self.redis.get(f"sam_cache_{cache_key}")
return json.loads(cached) if cached else None
def cache_result(self, cache_key, data, ttl=3600):
"""Cache result for 1 hour to reduce API usage"""
self.redis.setex(
f"sam_cache_{cache_key}",
ttl,
json.dumps(data)
)
Data Completeness Issues
SAM.gov API responses are incomplete. Production systems need additional data sources:
- Contract Descriptions: Must scrape SAM.gov website
- Award Information: Cross-reference with USASpending.gov
- Contact Details: Often missing emails/phones
- Historical Data: Limited to 1 year retention
# Web scraping for missing descriptions
from selenium import webdriver
from bs4 import BeautifulSoup
import time
def scrape_opportunity_description(opportunity_id):
"""Scrape contract description from SAM.gov website"""
# This is necessary because API doesn't provide descriptions
try:
driver = webdriver.Chrome() # Requires ChromeDriver
url = f"https://sam.gov/opp/{opportunity_id}/view"
driver.get(url)
# Wait for page load
time.sleep(3)
# Find description section
soup = BeautifulSoup(driver.page_source, 'html.parser')
description_element = soup.find('div', class_='description-text')
if description_element:
return description_element.get_text(strip=True)
else:
return "Description not found"
except Exception as e:
print(f"Scraping error: {e}")
return None
finally:
if driver:
driver.quit()
# This adds significant complexity and maintenance overhead
Why Most Developers Switch to Alternatives
Development Time Reality Check
| Task |
SAM.gov Implementation |
Alternative API |
| Setup & Registration |
2-6 weeks |
5 minutes |
| Basic Integration |
40+ hours (complex structure) |
2-4 hours (clean JSON) |
| Description Scraping |
20+ hours (Selenium, error handling) |
Included in response |
| Award Data Integration |
30+ hours (USASpending.gov) |
Included in response |
| Rate Limit Management |
10+ hours (Redis, monitoring) |
2,000 requests/hour |
| Error Handling |
15+ hours (complex edge cases) |
Standard HTTP patterns |
| Testing & Debugging |
20+ hours (rate limited) |
5 hours (unlimited testing) |
Total Development Time: 125+ hours vs 15 hours
At $50/hour: $6,250 vs $750 in developer costs
Alternative: GovCon API
Compare the same functionality with a developer-friendly API:
# GovCon API - Same functionality, 95% less code
import requests
def get_complete_opportunities():
"""Get complete opportunity data in one simple request"""
headers = {'Authorization': 'Bearer your_govcon_api_key'}
response = requests.get(
'https://govconapi.com/api/v1/opportunities/search',
headers=headers,
params={
'naics': '541330',
'state': 'CA',
'limit': 100 # 10x more data per request
}
)
if response.status_code == 200:
data = response.json()
for opp in data['data']:
print(f"Title: {opp['title']}")
print(f"Agency: {opp['agency']}")
print(f"Posted: {opp['posted_date']}")
print(f"Description: {opp['description_text'][:100]}...") # Included!
print(f"Contact: {opp['contact_email']}") # Complete contact info
print(f"Award Amount: ${opp['award_amount'] or 'TBD'}") # Integrated awards
print(f"Winner: {opp['awardee_name'] or 'TBD'}") # Winner information
print("---")
return data
else:
raise Exception(f"API Error: {response.status_code}")
# This is all you need - no registration, scraping, or complex parsing
opportunities = get_complete_opportunities()
print(f"Found {len(opportunities['data'])} opportunities")
Production Architecture Comparison
SAM.gov Production Architecture (Complex)
- API Client: Rate limiting, error handling, retries
- Web Scraper: Selenium grid for descriptions
- Data Pipeline: USASpending.gov integration
- Cache Layer: Redis for rate limit management
- Database: Store incomplete API responses + scraped data
- Monitoring: Track API quotas and scraper health
- Maintenance: Handle SAM.gov website changes
Infrastructure Cost: $200-500/month
Maintenance Hours: 5-10 hours/month
GovCon API Production Architecture (Simple)
- API Client: Simple HTTP requests
- Data Processing: Direct JSON parsing
- Optional Cache: For performance optimization
Infrastructure Cost: $19/month
Maintenance Hours: 0 hours/month
Conclusion
While SAM.gov's API is technically functional, the implementation complexity and limitations make it impractical for most business applications. The combination of:
- Lengthy registration process (2-6 weeks)
- Severe rate limits (10-1,000 requests/day)
- Missing critical data (descriptions, awards)
- Complex integration requirements (125+ hours)
- Ongoing maintenance overhead
Results in total costs exceeding $10,000 in the first year when including developer time, infrastructure, and ongoing maintenance.
Most successful federal contracting applications use specialized APIs that provide:
- Instant access without entity registration
- Production-ready rate limits (2,000 requests/hour)
- Complete data including descriptions and awards
- Simple integration (2-4 hours vs 125+ hours)
- Predictable monthly costs ($19 vs $1,000+/month)
Ready to Build Instead of Fight APIs?
Skip the complexity and get production-ready federal contract data in minutes, not months.
Start Free Trial
View Pricing
Last Updated: November 2025 | Contact: [email protected]