How to Find Expiring Federal Contracts (Recompetes) 6–18 Months Early with Python
List federal contracts whose period of performance ends in 6 to 18 months, filtered by category, agency and size, and get only the new ones each week.
Written with AI assistance and reviewed by the FedSignals Data team; we maintain these tools.
Most federal work isn't new work. It's the same requirement bought again when the current contract runs out. If you hear about a recompete only when the solicitation lands on SAM.gov, the incumbent has had years to shape it and you have 30 days to respond. Capture teams want those contracts on their radar 6 to 18 months earlier.
The data to do that is public. USAspending.gov publishes every federal prime contract award with its incumbent, agency, value, NAICS and PSC codes, and its period-of-performance end date. It's free and needs no API key.
The catch: USAspending lets you filter by when an award was signed or modified, but not by when it ends. Free guides from FedReady and Recompete make the same point. The usual workaround is to download active awards and sort the spreadsheet by end date yourself.
We built the Expiring Federal Contracts: Recompete Finder Actor on Apify to automate that workaround, and this tutorial walks through it. It pages through USAspending's contract search, keeps awards whose current end date falls in your window, and returns the soonest-ending first. A weekly run sends you only contracts you haven't seen.
What you'll build
- A list of contracts in your niche ending between 6 and 18 months from today, soonest first.
- Filters for category, agency, NAICS, place-of-performance state, keywords and minimum award size.
- A weekly run that returns only contracts not delivered before. If an end date moves, the contract comes back.
- A Slack message (or webhook to your CRM) with each new lead.
Step 1: Run it once by hand
Open apify.com/fedsignals/federal-recompete-finder and click Try for free. Here is the exact input we used for this article:
{ "categories": ["IT & Cyber"], "endsAfterDays": 180, "endsWithinDays": 540, "minAwardAmount": 5000000, "onlyNew": false, "maxItems": 5, "maxScan": 2000 } What each setting does:
-
endsAfterDays: 180andendsWithinDays: 540set the window: contracts ending 6 to 18 months from today. That's the range FedReady's guide suggests reviewing, early enough to talk to the buyer before the requirement is written. -
categoriesgroups PSC code families into 13 readable buckets such as "IT & Cyber", "Facilities & Maintenance", "Construction" or "Professional Services". For exact codes, usenaicsCodes(2 to 6 digits, for example5415). -
minAwardAmount: 5000000keeps contracts worth $5M or more. -
onlyNew: falseis for this first test. You'll turn it on in Step 3. -
maxItems: 5keeps the test small.maxScanis a safety cap on how many matching contracts are read per run.
You can also narrow by agencies (USAspending top-tier names such as Department of Veterans Affairs, or abbreviations such as VA), placeOfPerformanceStates, keywords, pscCodes (full codes such as D399 or prefixes such as D3), setAsides (for example sdvosb or 8(a)) and incumbents (company names or UEIs, to watch a competitor's contracts). A misspelled agency name stops the run with a "did you mean" hint before any contract is charged (only Apify's $0.00005 run-start fee applies).
Step 2: Look at the output
Our run of build 0.1.13 on Apify on Oct 31, 2026 finished in about 33 seconds; run time depends mostly on how fast USAspending responds. Its status line read 5 expiring contracts delivered, 15 API requests (maxItems 5 reached). Stopped at the maxItems limit (5) partway through contracts ending 2027-04-30, the window's first day; raise maxItems or your maximum charge to get the rest of the window. Here is the first result, trimmed:
{ "awardId": "47QFRA24F0005", "recipient": "CACI, INC. - FEDERAL", "awardAmount": 977933614.86, "currency": "USD", "awardType": "DELIVERY ORDER", "awardingAgency": "General Services Administration", "awardingSubAgency": "Federal Acquisition Service", "startDate": "2024-05-01", "endDate": "2027-04-30", "daysUntilEnd": 180, "category": "IT & Cyber", "naicsCode": "541512", "pscCode": "DA01", "placeOfPerformanceState": "VA", "description": "CDM DEFEND GROUP A BRIDGE TASK ORDER", "usaspendingUrl": "https://www.usaspending.gov/award/CONT_AWD_47QFRA24F0005_4732_47QTCK18D0009_4732", "samSearchUrl": "https://sam.gov/search/?index=opp&...keywordTags%5D%5B0%5D%5Bvalue%5D=47QFRA24F0005", "note": "Current period-of-performance end; options or extensions may move it." } The other four results:
| Incumbent | Buying office | Value | Ends |
|---|---|---|---|
| Accenture Federal Services | Department of the Army | $896.5M | Apr 30, 2027 |
| Booz Allen Hamilton | Federal Acquisition Service (GSA) | $710.6M | Apr 30, 2027 |
| Leidos | Bureau of Alcohol, Tobacco, Firearms and Explosives | $639.9M | Apr 30, 2027 |
| Dell Federal Systems | Defense Information Systems Agency | $218.6M | Apr 30, 2027 |
All five end on April 30, 2027, the first day of the window. Results come soonest-ending first, and contracts that end on the same day come largest first.
usaspendingUrl opens the award, where you can check the potential end date, the competition type and the number of offers. samSearchUrl searches SAM.gov for the award ID, which is how you spot sources-sought notices or a follow-on solicitation.
USAspending doesn't always report an awarding agency. In an earlier test, one Deloitte row came back with awardingAgency set to null. Your code should expect that (see Step 3).
The run also saves a summary in its key-value store (record SUMMARY):
{ "window": ["2027-04-30", "2028-04-24"], "pushed": 5, "skippedAlreadySeen": 0, "apiRequests": 15, "matchesInWindow": 118, "windowFullyCovered": false, "stopReason": "maxItems", "lastDateCovered": null } stopReason says why the run stopped: complete, maxItems, matchLimit (your maxScan), spendLimit (your maximum charge), apiPagingLimit or error. lastDateCovered is the last end date for which every matching contract from the start of the window was delivered. Here it's null because more than five contracts end on the window's first day, so the run stopped partway through that day. If fewer contracts end on your window's first day, your status line says "fully covered contracts ending ... through ..." instead and lastDateCovered shows that last date. matchesInWindow counts the matching contracts read before the run stopped, not the total in the window, and windowFullyCovered is true only when every matching contract in the window was read. A small maxItems test like this one is never fully covered; raise maxItems to get more of the window.
USAspending pages through at most 10,000 results per sort order, so the Actor reads from both ends of the window and can cover up to 20,000 contracts per search. If part of the window is out of reach, unreadRange lists the end dates the Actor couldn't read. Add a category, agency or state, or raise minAwardAmount, to narrow a search like that.
Step 3: Call it from Python
This snippet runs the Actor synchronously and prints each new lead. It reads your Apify token from an environment variable (never paste it into code). maxTotalChargeUsd is a hard spending cap for the run.
import os import requests ACTOR = "fedsignals~federal-recompete-finder" URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items" run_input = { "categories": ["IT & Cyber"], "agencies": ["Department of Veterans Affairs"], "endsAfterDays": 180, "endsWithinDays": 365, "minAwardAmount": 10000000, "onlyNew": True, # only contracts this stateKey hasn't returned before "stateKey": "va-it-recompetes", # one key per saved search "maxItems": 10, } resp = requests.post( URL, params={"maxTotalChargeUsd": 0.15}, headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"}, json=run_input, timeout=300, ) resp.raise_for_status() for n in resp.json(): agency = n.get("awardingSubAgency") or n.get("awardingAgency") or "agency not reported" amount = n.get("awardAmount") or 0 print(f"{n['endDate']} {n['awardId']} ${amount:,.0f} {n['recipient']} ({agency})") Run it twice. The second run doesn't repeat anything from the first: onlyNew remembers every contract it delivered for that stateKey and moves on to the next ones. In our test, VA IT contracts of $10M+ ending in 6 to 12 months matched 117 awards. A small maxItems on a weekly schedule gives you a batch you can actually work through.
Step 4: Schedule it weekly
End dates don't change daily, so a weekly run is enough.
- In the Apify console, click Save as a new task, enter your filters, set
onlyNew: trueand give it astateKey. - Go to Schedules → Create new, use the cron expression
0 7 * * 1(Mondays at 7:00), set your timezone, and attach the task.
Keep endsAfterDays and endsWithinDays relative, as they are. The window rolls forward each week by itself.
Step 5: Send leads to Slack or a webhook
The simplest option is Apify's built-in Slack integration on the task (Integrations → Slack). If you want one line per contract, add a webhook for ACTOR.RUN.SUCCEEDED that points at a small function you host. Apify's default payload includes resource.defaultDatasetId:
import os import requests def handler(payload: dict) -> None: dataset_id = payload["resource"]["defaultDatasetId"] items = requests.get( f"https://api.apify.com/v2/datasets/{dataset_id}/items", headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"}, timeout=60, ).json() if not items: return # nothing new this week lines = [f"*{len(items)} federal contracts entering the recompete window*"] for n in items[:20]: agency = n.get("awardingSubAgency") or n.get("awardingAgency") or "agency n/a" amount = n.get("awardAmount") or 0 lines.append( f"• <{n['usaspendingUrl']}|{n['recipient']}> ({agency}), " f"${amount / 1e6:,.1f}M, ends {n['endDate']} · <{n['samSearchUrl']}|SAM.gov>" ) requests.post(os.environ["SLACK_WEBHOOK_URL"], json={"text": "\n".join(lines)}, timeout=30) The same webhook can feed Google Sheets, HubSpot, Zapier or Make.
What to do with a lead
An end date is a reason to start asking questions, not a promise of a solicitation. Agencies exercise options, issue bridge contracts, or move work onto a larger vehicle. For each lead:
- Open
usaspendingUrl. If the potential end date is years past the current one, the next option is probably coming, not a recompete. - Check the agency's procurement forecast and open
samSearchUrlfor sources-sought notices. - Send the contracting office your capability statement, and decide early whether you'll prime or team with the incumbent.
When the solicitation does post, our SAM.gov alerts tutorial shows how to get it in Slack the morning after.
What it costs
The Actor uses pay-per-event pricing: $0.01 per expiring contract delivered, plus Apify's $0.00005 run-start fee. The 5-contract test above costs about $0.05. The Python example is capped at 10 contracts, about $0.10 a week. A week with nothing new costs only the start fee, because onlyNew skips contracts you've already received.
FAQ
Do I need a USAspending API key?
No. The USAspending API is public and keyless. You only need a free Apify account to run the Actor.
How current is the data?
As current as USAspending. Department of Defense contract data is published with a 90-day delay, so the newest DoD awards appear later.
Does an end date mean the contract will be recompeted?
No. endDate is the current period-of-performance end. Options or extensions can move it, and IDIQ orders may be re-issued under the same vehicle. If an end date changes, onlyNew returns that contract again so you see the update.
Can I search by NAICS instead of category?
Yes. Leave categories empty and set naicsCodes, for example ["541512", "541519"].
Can I run several searches?
Yes. Save one task per search, each with its own stateKey.
Related
- How to Get Daily SAM.gov Contract Opportunity Alerts in Slack: catch the solicitation when it posts.
- USAspending Scraper: Federal Spending Trends by Category: see which categories and agencies are growing before you pick targets.
Last updated: Nov 2, 2026 (tested against Actor build 0.1.13).
FedSignals Data builds small public-data monitors on Apify. FedSignals is not affiliated with, or endorsed by, the U.S. government or USAspending.gov. Questions or feature requests: fedsignals.alerts@gmail.com.