Skip to content

Performance

Operational Notes

  • Concurrent Requests: 5-15 recommended for best performance
  • Data Updates: daily (new postings and modifications). Check current_as_of on /api/v1/status for current freshness.
  • Compression: responses are gzip-encoded when your client sends Accept-Encoding: gzip.

Best Practices for High-Volume Usage

Expect geographic variation in response times:

curl -w "Total: %{time_total}s (Network: ~%{time_connect}s + Server: ~%{time_starttransfer}s)\n" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  "https://govconapi.com/api/v1/opportunities/search?naics=541330&limit=100"

Use pagination within a filtered query:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://govconapi.com/api/v1/opportunities/search?naics=541330&limit=100&offset=0"

For keeping a local mirror in sync, use /api/v1/opportunities/delta instead. It filters by last_seen via a since cursor (use the returned sync.server_time as your next since), so you only pull what changed.

Monitor your usage to stay within rate limits:

  • Free plan: 50 requests/day
  • Developer: 1,000 requests/hour

A second, per-minute burst cap runs alongside your plan quota. It is 5% of your rate limit, floored at 15/minute: 50 requests/minute on Developer/Pro (5% of 1,000/hour) and 15/minute on Free, measured in a rolling 60-second window. A tight loop can trip the burst cap while you are still well under your plan total, a response can report quota left in X-RateLimit-Remaining and still return 429. The burst window clears within 60 seconds: a burst 429 carries Retry-After: 60, so a small delay between calls (~1.2s) or a brief backoff on the first 429 resolves it.

Every authenticated response reports your current rate-limit state in headers, so you can throttle before you hit the ceiling instead of reacting to a 429:

  • X-RateLimit-Limit, your plan's request ceiling for the window.
  • X-RateLimit-Remaining, requests left in the current window. Slow down as this approaches 0.

On a 429 response we also send Retry-After (in seconds), which tells you exactly when to resume. The pattern: back off on X-RateLimit-Remaining, recover on Retry-After.

Use specific filters to reduce server processing time:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://govconapi.com/api/v1/opportunities/search?naics=541330&posted_after=2025-11-01"

Note: NAICS filtering is well-optimized and faster than generic searches

Error Handling Best Practices

import requests
import time

def make_api_request(url, headers):
    response = requests.get(url, headers=headers)

    # Proactive: slow down before you hit the ceiling.
    remaining = int(response.headers.get('X-RateLimit-Remaining', 1))
    if remaining < 10:
        time.sleep(1)  # ease off as the window runs low

    if response.status_code == 429:
        # Rate limit exceeded: Retry-After tells you exactly when to resume.
        retry_after = int(response.headers.get('Retry-After', 3600))
        print(f"Rate limited. Waiting {retry_after} seconds...")
        time.sleep(retry_after)
        return make_api_request(url, headers)  # Retry

    elif response.status_code == 402:
        # Plan upgrade needed
        error = response.json()
        print(f"Upgrade required: {error['detail']}")
        return None

    return response.json()
Technical Architecture (Click to expand)

Rate Limiting

  • Free: 50 requests/day
  • Developer: 1,000 requests/hour
  • Plan Restrictions: page size (100 vs 1,000) enforced by plan level. All search filters available on every plan.

Performance

  • Indexed filters: agency, NAICS, PSC, dates, and full-text are indexed; specific filters return faster than broad keyword-only scans.
  • NAICS filtering is among the fastest paths; prefer it when you can.
  • Data size: Loading... opportunities, refreshed daily.
  • Transport: HTTPS only; responses gzip-encoded on Accept-Encoding: gzip.

Data source & freshness

  • Source: SAM.gov (opportunities, awards, entity registry, exclusions) plus contracting-officer contacts.
  • Refresh: daily for opportunities/awards; the SAM entity registry is a monthly snapshot; exclusions refresh daily. Check /api/v1/status for current freshness.

Production Usage Guide

Python client with pagination & error handling (Click to expand)

# Production-ready integration pattern
import requests
import time
from datetime import datetime, timedelta

class GovConAPIClient:
    def __init__(self, api_key, plan='developer'):
        self.api_key = api_key
        self.base_url = 'https://govconapi.com/api/v1'
        self.headers = {'Authorization': f'Bearer {api_key}'}

        # Configure based on your plan
        if plan == 'developer':
            self.rate_limit = 1000  # 1,000 / hour
            self.burst_limit = 50   # 50 / minute (5% of the 1,000/hour rate limit)
        else:
            self.rate_limit = 50    # 50 / day (free)
            self.burst_limit = 15   # 15 / minute (the burst floor)

    def search_opportunities(self, filters=None, limit=100):
        """Search with automatic pagination and rate limiting.
        Every filter is optional, but a filtered search pages far less."""
        all_results = []
        offset = 0
        while True:
            params = {'limit': limit, 'offset': offset}
            if filters:
                params.update(filters)
            response = self._make_request('/opportunities/search', params)
            if not response or 'data' not in response:
                break
            all_results.extend(response['data'])
            # Use the explicit pagination.has_next signal returned by the API.
            if not response.get('pagination', {}).get('has_next'):
                break
            offset += limit
            time.sleep(0.1)  # gentle on the rate limit
        return all_results

    def _make_request(self, endpoint, params=None):
        """Make request with proper error handling"""
        url = f"{self.base_url}{endpoint}"

        try:
            response = requests.get(url, headers=self.headers, params=params)

            if response.status_code == 429:
                retry_after = int(response.headers.get('Retry-After', 3600))
                print(f"Rate limited. Waiting {retry_after} seconds...")
                time.sleep(retry_after)
                return self._make_request(endpoint, params)

            response.raise_for_status()
            return response.json()

        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}")
            return None

# Example usage
client = GovConAPIClient('your_api_key_here', 'developer')

# Search for DoD opportunities in the last 30 days
filters = {
    'agency': 'Department of Defense',
    'posted_after': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
}

opportunities = client.search_opportunities(filters)
print(f"Found {len(opportunities)} opportunities")
    

Load Testing Recommendations

  • Burst Testing: Developer/Pro: up to 50 requests/minute (5% of the 1,000/hour quota) in any 60-second window
  • Sustained Load: Stay under 80% of your hourly limit for consistent performance
  • Concurrent Requests: 5-15 concurrent requests recommended for optimal reliability
  • Response Caching: Cache responses for 10-15 minutes to reduce API calls
  • Geographic Considerations: Network latency varies 50-400ms by location
  • Performance Monitoring: Use curl timing options to distinguish network vs server performance
Enterprise Integration Patterns (Click to expand)

Pattern 1: Daily Sync with the delta endpoint

Use /api/v1/opportunities/delta, not /search, for keeping a local copy in sync. Delta returns every record changed since your since timestamp (including backfilled records that posted_after would miss) and pages cleanly with has_next.

def daily_sync(client, last_sync_time):
    """Pull every opportunity that's been added or updated since last_sync_time."""
    offset = 0
    new_server_time = None
    while True:
        resp = client._make_request('/opportunities/delta', {
            'since': last_sync_time,
            'limit': 1000,
            'offset': offset,
        })
        if not resp:
            break
        for op in resp['data']:
            store_opportunity(op)   # upsert by notice_id
        # Capture the server's clock from the FIRST page so we use a single
        # consistent cutoff across all paginated calls in this run.
        if new_server_time is None:
            new_server_time = resp['sync']['server_time']
        if not resp['pagination']['has_next']:
            break
        offset += 1000
    return new_server_time   # save this; pass as last_sync_time tomorrow

Pattern 2: Agency Monitoring

Watching N agencies? Make one delta call (or one search call), filter agencies client-side. Looping the API once per agency burns the rate limit and adds latency for no benefit.

def monitor_agencies(client, agencies_of_interest, last_sync_time):
    """Pull all changes since last sync, route by agency client-side."""
    of_interest = {a.lower() for a in agencies_of_interest}
    resp = client._make_request('/opportunities/delta', {
        'since': last_sync_time,
        'limit': 1000,
    })
    if not resp:
        return
    for op in resp['data']:
        agency = (op.get('agency') or '').lower()
        if any(target in agency for target in of_interest):
            if is_new_opportunity(op):
                send_notification(op)
    return resp['sync']['server_time']

Pattern 3: Map a prime contractor's supply chain

For BD intel, M&A diligence, and set-aside compliance: combine prime award history with the subaward layer. Two calls give you "what they won as a prime" + "who they paid as subs" + a leaderboard of the top sub-vendors.

def supply_chain_map(client, prime_uei):
    """Profile a prime contractor's federal footprint, top to bottom."""
    # 1. Prime-side awards: what contracts have they won?
    profile = client._make_request(f'/companies/{prime_uei}', {})
    awards  = client._make_request(f'/companies/{prime_uei}/awards', {'limit': 1000, 'sort': 'amount'})

    # 2. Sub-side flow: who have they paid, and how much?
    # Page size is up to 1,000 on paid plans, the same on every list endpoint.
    # For a top prime with thousands of FFATA reports, page through with offset.
    subs = client._make_request(f'/companies/{prime_uei}/subawards', {'limit': 1000})

    return {
        'prime_name':           profile['name'],
        'total_won_as_prime':   profile['total_value'],            # all-time award notices
        'total_paid_to_subs':   subs['summary']['total_subaward_amount'],
        'distinct_sub_vendors': subs['summary']['distinct_sub_vendors'],
        'distinct_contracts':   subs['summary']['distinct_prime_contracts'],
        'subaward_date_range':  (subs['summary']['first_subaward_date'],
                                 subs['summary']['last_subaward_date']),
        'top_5_subs_by_dollars': _top_n_subs(subs['data'], 5),
        'top_5_awards':         awards['data'][:5],
    }

For the per-contract supply-chain forensics (every sub on one prime PIID), use /subawards/search?piid=<piid>. For the agency or NAICS slice across all primes, use the same endpoint with those filters instead of a UEI.

Pattern 4: True federal revenue for a small business

A small business in federal contracting often earns the larger half of its federal income as a sub, not as a prime. Looking up /companies/{uei}/awards alone undercounts. The honest revenue view is the union:

def true_federal_revenue(client, uei):
    """Combined prime + sub revenue picture. /companies/{uei} returns this in one call."""
    profile = client._make_request(f'/companies/{uei}', {})

    return {
        'name':                 profile['name'],
        'prime_revenue':        profile['total_value'],         # SAM Award Notice value (see fpds_obligated_total for the fuller FPDS prime total)
        'sub_revenue':          profile['sub_revenue_total'],   # subaward dollars received, current FFATA coverage
        'combined':             profile['total_value'] + profile['sub_revenue_total'],
        'prime_share':          profile['prime_revenue_share'], # 0..1; null when both are zero
        'top_paying_primes':    profile['top_paying_primes'],   # who their actual customers are
        'top_naics_as_prime':   profile['top_naics'],
    }

# Example output for an SMB:
#   prime_revenue = $1.2M,  sub_revenue = $8.4M,  combined = $9.6M,  prime_share = 0.13
#   → 87% of their federal revenue is sub work; the prime-only view missed it.

Two different prime measures appear here: this snippet's hand-combined combined uses total_value (the SAM Award Notice slice) for prime, while the response's prime_revenue_share field uses the broader FPDS prime obligation total (fpds_obligated_total). Both the FPDS prime total and the FFATA subaward total cover the FY2025-onward window (since 2024-10-01), so prime_revenue_share compares like-for-like. For the authoritative prime figure use fpds_obligated_total; for the underlying records, see /companies/{uei}/prime-relationships.

Pattern 5: Active-payment compliance screen on an excluded vendor

For compliance and FCA investigation: when an entity hits the SAM exclusion list, the question that matters is whether clean primes are still paying them. /exclusions/{uei_sam} answers both halves of the screen, the exclusion record itself, and the recent-payments enrichment, in one call.

def active_payment_screen(client, uei):
    """Returns the exclusion record + the smoking-gun recent-payments view,
    or None if the UEI is clean by UEI lookup (still pair with name search for full coverage)."""
    resp = client.get(f'/api/v1/exclusions/{uei}')
    if resp.status_code == 404:
        return None   # no UEI-keyed exclusion; check by entity_name for full coverage

    body = resp.json()
    return {
        'exclusion_type':           body['exclusion_type'],
        'excluding_agency':         body['excluding_agency_code'],
        'create_date':              body['create_date'],
        'active':                   body['record_status'] == 'Active',
        # The compliance signal:
        'still_being_paid':         body['recent_sub_payments']['subaward_count'] > 0,
        'total_received_last_12mo': body['recent_sub_payments']['total_received'],
        'most_recent_payment':      body['recent_sub_payments']['most_recent_subaward_date'],
        'most_recent_paying_prime': body['recent_sub_payments']['most_recent_paying_prime'],
        'top_paying_primes':        body['recent_sub_payments']['top_paying_primes'],
    }

Pair this with /exclusions/search?entity_name=... for full screening coverage: ~80% of SAM exclusions are individuals or entities with no UEI, so a UEI lookup alone is not a full screen. For the broader risk surface (address clusters, name variants, coordinated waves), the same enrichment is also present on /vendor-risk/{uei} under subaward_exposure.

A REST API and MCP server for U.S. federal procurement data.

Get a free key, emailed instantly, or follow the quickstart.

Found something wrong, missing or confusing on this page?

Press Ctrl + Enter to send a note. Highlight text first and it travels with your note.