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):

  1. Create free SAM.gov account
  2. Generate API key (instant)

Step 2: Entity Registration for 1,000/day (~2-4 weeks)

For higher limits:

  1. Complete entity registration with UEI
  2. Wait for validation (10-15 business days)
  3. Request "Data Entry" role
  4. Wait for role approval (5-30 business days)
  5. Regenerate key with higher limits

Step 2: Generate API Key

Once approved:

  1. Log into SAM.gov
  2. Navigate to "Data Services" → "API Information"
  3. Generate new API key
  4. 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

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

# 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:

# 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")

Skip 125 Hours of Development Time

Get the same data with 95% less code and zero registration hassle.

Get Instant API Key View Simple Documentation

Production Architecture Comparison

SAM.gov Production Architecture (Complex)

Infrastructure Cost: $200-500/month

Maintenance Hours: 5-10 hours/month

GovCon API Production Architecture (Simple)

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:

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]