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.
What this guide covers: Step-by-step API implementation including authentication, endpoints, code examples, and error handling. Estimated implementation time varies based on your requirements.
Building a Complete Solution?
Once you have opportunities, get verified contacts for contracting officers & decision makers. Try GovCon Contacts →
Table of Contents
Authentication & API Keys
API Key Required for All Access
SAM.gov API requires an api_key query parameter for all requests (even the 10/day limit):
curl "https://api.sam.gov/prod/opportunities/v2/search?api_key=your_sam_key&limit=1&postedFrom=01/01/2026&postedTo=01/28/2026"
Step 1: Create SAM.gov Account & Generate Key (Instant)
Basic access (10/day):
- Create free SAM.gov account
- Generate API key (instant)
Step 2: Entity Registration for 1,000/day (~2-4 weeks)
For higher limits:
- Complete entity registration with UEI
- Wait for validation (10-15 business days)
- Request "Data Entry" role
- Wait for role approval (5-30 business days)
- Regenerate key with higher limits
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 "https://api.sam.gov/prod/opportunities/v2/search?api_key=$SAM_API_KEY&limit=1&postedFrom=01/01/2026&postedTo=01/28/2026"
API Endpoints & Parameters
Primary Endpoints
| Endpoint |
Purpose |
Rate Limit Impact |
/opportunities/v2/search |
Search contract opportunities |
1 request per search |
/opportunities/v2/search?noticeId=... |
Get specific opportunity by ID |
1 request per lookup |
/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
title - Search in opportunity title
solnum - Solicitation number
noticeId - Specific opportunity ID
postedFrom / postedTo - Date range (MM/DD/YYYY)
ptype - Notice type (p = Presolicitation, o = Solicitation, a = Award Notice, etc.)
typeOfSetAside - Set-aside type code
ncode - NAICS code filter
ccode - Classification code filter
state - State abbreviation for place of performance
limit - Results per page (max 1000)
offset - Pagination offset
Note: There is no keyword parameter for full-text search. The API only supports title search.
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/prod"
self.session = requests.Session()
self.session.headers.update({
'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 - api_key goes in query params, not header
default_params = {
'api_key': self.api_key,
'limit': 10, # Small limit due to rate constraints
'offset': 0,
'postedFrom': (datetime.now() - timedelta(days=30)).strftime('%m/%d/%Y'),
'postedTo': datetime.now().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 code - API returns both naicsCode (string) AND naicsCodes (array)
'naics': opp.get('naicsCode'), # e.g., "541512" - also check naicsCodes array
# Solicitation number
'solicitation_number': opp.get('solicitationNumber'),
# Award info (if available)
'award': self._extract_award_info(opp.get('award', {})),
# Description is a URL link, not the text itself
# Append your API key to download: description_url + '?api_key=YOUR_KEY'
'description_url': opp.get('description'),
# 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, notice_id):
"""Get detailed information for a specific opportunity.
Note: There is NO /opportunities/v2/{id} endpoint.
Use the search endpoint with noticeId parameter instead.
"""
if self.rate_limit_remaining <= 0:
raise Exception("Daily rate limit exceeded")
# Use search with noticeId parameter - the only way to get specific opportunity
return self.search_opportunities(noticeId=notice_id)
# Usage example
def main():
# Initialize client
api_key = "your_sam_gov_api_key"
client = SAMAPIClient(api_key)
try:
# Search for recent opportunities by title
# Note: No full-text "keyword" parameter exists - use "title" instead
result = client.search_opportunities(
title="software", # Searches in title field only
ptype="o", # Solicitations only
limit=5
)
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: {opp['naics'] or 'None'}") # naicsCode (string) - also naicsCodes array available
print(f"Contact: {opp['contact']['email'] if opp['contact'] else 'None'}")
print(f"Description URL: {opp['description_url'] or 'None'}") # URL to download
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/prod';
this.rateLimitRemaining = 10; // Track daily limit
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Accept': 'application/json'
},
timeout: 30000
});
}
async searchOpportunities(params = {}) {
if (this.rateLimitRemaining <= 0) {
throw new Error('Daily rate limit exceeded (10 requests)');
}
// api_key goes in query params, not header
const defaultParams = {
api_key: this.apiKey,
limit: 10,
offset: 0,
postedFrom: this._getDateString(30), // Last 30 days
postedTo: this._getDateString(0) // Today
};
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, // String - also naicsCodes array available
solicitationNumber: opp.solicitationNumber,
// Description is a URL - append ?api_key=YOUR_KEY to download
descriptionUrl: opp.description,
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 {
// Note: No "keyword" parameter - use "title" for title search
const result = await client.searchOpportunities({
title: 'software', // Searches title field only
ptype: 'o', // Solicitations
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 URL: ${opp.descriptionUrl || 'None'}`);
});
} 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: Linked when available (often requires separate lookup)
- Contact Details: Often missing emails/phones
- Date Range: Limited to 1 year per query (use multiple queries for older data)
# 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 Linking |
Manual correlation required |
Included in response |
| Rate Limit Management |
10+ hours (Redis, monitoring) |
1,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: Data ingestion & normalization
- 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
Summary
This guide covered the complete SAM.gov API implementation including authentication, endpoints, parsing, and production deployment. Key takeaways:
- Entity registration takes about 10 business days but provides 1,000 requests/day
- Public access (10/day) is available immediately for testing
- Response parsing requires handling nested JSON structures
- Caching is essential for staying within rate limits
For high-volume commercial applications, third-party APIs like GovCon API offer faster setup and higher limits as an alternative.
Related Guides
Official Resources
Last Updated: January 2026 | Questions? [email protected]