TX ESBD Opportunity Search avatar

TX ESBD Opportunity Search

Pricing

$6.00 / 1,000 solicitation records

Go to Apify Store
TX ESBD Opportunity Search

TX ESBD Opportunity Search

Find Texas government contracts by keyword, NIGP code, agency or status over 59K+ solicitations. Best starting point for finding bids. AI-agent ready.

Pricing

$6.00 / 1,000 solicitation records

Rating

0.0

(0)

Developer

Wenhao Yang

Wenhao Yang

Maintained by Community

Actor stats

0

Bookmarked

6

Total users

3

Monthly active users

3 days ago

Last modified

Categories

Features

Limited permissions

Share

TX Government Contracts & Bids Search, NIGP (tx-esbd-search)

Search every Texas state & local government solicitation - the official Texas Electronic State Business Daily (ESBD) record - by keyword, NIGP commodity code, agency or status, and get back structured JSON with the NIGP codes already parsed apart.

This is the starting point of the Texas procurement family: you have a category or a set of keywords, and you want to know what the state is buying right now.

Low cost

$0.006 per record - you are charged per matching record delivered, never for the query itself. The scan stops early once it has found limit matches, so the cost tracks what you actually receive rather than the depth of the corpus.

What you get

Every match is normalized with structured NIGP codes wherever the source publishes them - not the raw "94828-Dental Services;" string. (Older solicitations sometimes carry no NIGP code at all; they are still returned, and an NIGP filter excludes them - see the note below.)

{
"platform": "tx-esbd",
"agency": "City of Waco - M1612",
"agencyNumber": "M1612",
"title": "RFB 2026-062 District 3 Operational Street Maintenance & Pavement Rehabilitation",
"bidNumber": "RFB2026-062",
"status": "Posted",
"publishedDate": "11/4/2026",
"closeDate": "12/18/2026 2:00 PM",
"nigpCodes": [
{ "code": "91276", "description": "Striping Streets, Parking Facilities, Lane Divisions, Etc. (Paint)" },
{ "code": "91319", "description": "Construction, Curb And Gutter (Includes Maintenance, Repair, And Removal)" },
{ "code": "91327", "description": "Construction, Highway And Road" },
{ "code": "91345", "description": "Construction, Sewer And Storm Drain" },
{ "code": "91356", "description": "Construction, Utility/Underground Projects" },
{ "code": "91371", "description": "Maintenance And Repair, Highway And Road (To Include The Removal Of Asphalt, Concrete, Bitumens, Etc)" },
{ "code": "91381", "description": "Maintenance And Repair, Sewer And Storm Drain (Including Removal)" },
{ "code": "91396", "description": "Paving/Resurfacing, Street (Major And Residential)" },
{ "code": "96861", "description": "Pavement Marking Services (Including Removal Of Markings)" },
{ "code": "98852", "description": "Landscaping (Including Design, Fertilizing, Planting, Etc., But Not Grounds Maintenance Or Tree Trimming Services)" }
],
"url": "https://waco-texas.bonfirehub.com/portal/",
"internalId": "573390",
"created": "11/4/2026 9:27 am",
"lastModified": "11/4/2026 9:31 am"
}

Fields: platform, agency, agencyNumber, title, bidNumber, status, publishedDate, closeDate, nigpCodes (structured), url, internalId, created, lastModified.

Statuses: Posted / Awarded / Addendum Posted / Closed / No Award / Posting Cancelled / No Award Reposted

Filters

FilterWhat it does
keywordsSubstring match on the title, agency, bid number and the NIGP descriptions - searching dental also finds solicitations classified under NIGP Dental Services even when the title never says "dental".
nigpCodesRestrict to one or more 5-digit NIGP commodity codes (exact match). A solicitation with no published NIGP code is excluded by this filter.
agenciesRestrict to one or more agencies, matched exactly against the agency number ("529") or the full agency name as published. A partial name does not match.
statusesExact, case-sensitive match on any of the lifecycle statuses above (Posted, not posted).
publishedAfter / publishedBeforeDate window (YYYY-MM-DD) - a filter over the scanned pages, not a server-side bound.
pagesScan depth from the newest record (24 records per page). Default 20; the 1,500-second run budget usually covers several hundred pages on the platform's network - _meta.scannedPages reports what a run actually covered.
limitHard cap: the scan stops as soon as this many matches are found. Default 50. _meta.limitReached reports it; more matches may exist beyond the scanned prefix.

Example inputs

Everything under one NIGP commodity code - the most common way to watch a trade. nigpCodes matches the register's 5-digit commodity codes exactly; 66 of the newest 480 solicitations carried 91371 (highway maintenance & repair) when measured on 2026-11-08, so the default 20-page scan finds matches without going deep.

{ "nigpCodes": ["91371"], "limit": 50 }

Two things trip people up here: a 3-digit class (909) is not a commodity code and matches nothing - pass the 5-digit code, or use keywords (which searches NIGP descriptions too). And a tight code on a quiet week can legitimately match nothing in the newest ~500 records - raise pages (or use publishedAfter/publishedBefore) before concluding there is no such bid.

What the state is buying in a category - keywords reaches the NIGP descriptions too, so one word finds solicitations whose title never uses it.

{ "keywords": ["dental"], "limit": 50 }

Open bids from one agency:

{ "agencies": ["529"], "statuses": ["Posted"], "limit": 100 }

One specific code (e.g. painting services):

{ "nigpCodes": ["91054"], "statuses": ["Posted"], "pages": 200 }

A date window - pair a scan with publishedAfter / publishedBefore to cover a date range. Set limit too: the scan stops as soon as limit matches are in hand, so the default (50) returns only 50 records even across a wide window (max limit is 500 per run):

{ "pages": 2600, "limit": 500, "publishedAfter": "2026-01-01", "publishedBefore": "2026-03-31" }

Why this is hard

The source is a live JSON service with no server-side search: the lines filter parameter is accepted and then ignored, so every query is a client-side pass over the corpus, and the corpus is ordered by last modified, not posted date. That means a shallow scan covers the newest-modified records but can miss an older solicitation that has not been modified recently - which is why the run reports the date range it actually covered, so you can tell a genuinely empty result from a scan that stopped too early.

Scanning the full corpus

pages is scan depth from the newest record, not a page offset. The source serves 24 records per page (~60,400 records total), one request per page, and there is no server-side filter - so a deep scan is a long series of requests and is bounded by the actor's run time limit (default 30 minutes, of which the scan may use 25), not by pages alone. Two things stop a scan early: the run-time budget and limit; both are reported in _meta (truncated / limitReached / stopReason), so a partial answer is visible rather than silent.

Because of that, a single run is not the way to cover the whole corpus. A filter narrows what counts as a match, but every run still scans from the newest record; a historical window deeper than the run-time budget cannot be fully covered in one run. Use a date window or NIGP code to keep the matches near the top of the scan (the source's modified-date ordering is the reason date windows beat plain deep scans):

{"pages": 800, "publishedAfter": "2026-01-01", "publishedBefore": "2026-03-31"}
{"pages": 200, "nigpCodes": ["91054"], "statuses": ["Posted"]}

The run also emits one extra _meta dataset row (not charged) reporting how many unique records were scanned, the source's total record count, the pages actually scanned, whether the scan is partial (truncated, set when the run-time budget or limit stopped it), whether limit stopped it (limitReached), why (stopReason), whether the charge cap stopped delivery (chargeLimitReached), and the posted/modified date range the scanned prefix covers. The prefix is ordered by last modified, not posted date, so treat scanModDate* as the coverage signal and scanPostDate* as context.

Per-page latency depends on where the run executes; a few hundred pages per run is a comfortable working size.

Example output

{"keywords": ["painting"]} - a match, with the NIGP codes already parsed apart (measured 2026-11-05):

{
"platform": "tx-esbd",
"agency": "Department of Public Safety",
"agencyNumber": "405",
"title": "Houston HQ 1st Floor Hallway Refresh",
"bidNumber": "405-27R0002724",
"status": "Addendum Posted",
"publishedDate": "10/27/2026",
"closeDate": "11/30/2026 5:00 PM",
"nigpCodes": [
{
"code": "91054",
"description": "Painting, Maintenance And Repair Services (Including Caulking)"
}
],
"url": "https://texasdps.appianportalsgov.com/vendor-management",
"internalId": "571878",
"created": "10/27/2026 10:01 am",
"lastModified": "10/29/2026 11:11 am"
}

A run also emits a _meta record reporting the date range the scan actually covered, so an empty result can be told apart from a scan that stopped too early.

The rest of the Texas family

One actor, one job - match your goal:

Your goalActor
Find bids by keyword, NIGP, agency, statustx-esbd-search - you are here
Check one specific bid by its numbertx-esbd-get
Monitor new & changed solicitations on a scheduletx-esbd-changes
Who won state contracts, at what amountstx-esbd-awards
Find / vet a vendor - HUB, SDV, small business, NIGP capabilitytx-cmbl-vendors
Find disabled-veteran (DV) HUB vendorstx-hub-certified
Statewide contracts - who's on each SPD/TxSmartBuy contracttx-smartbuy-contracts
Pull the raw full corpus (query + monitor) - power userssled-tx-procurement

Notes

  • Data source: Texas Comptroller - ESBD. Public state records.
  • No login, no anti-bot - uses the official public JSON API.
  • Full corpus - ~60,400 solicitations (posted, awarded, closed & historical). For a complete historical sweep in one product, use sled-tx-procurement (raw corpus + monitor).
  • NIGP coverage varies by age - newer solicitations almost all carry NIGP codes; a 2019 slice measured on 2026-11-08 had them on 62% of rows (2020: 82%). An nigpCodes filter excludes a record that publishes no code, while keywords and the other filters still return it.
  • Empty arrays mean "not supplied" - {"statuses": []} is not a filter that matches zero rows; it scans unfiltered. Omit a key instead of passing an empty list.
  • A source error mid-scan fails the run rather than delivering a partial answer (the scan stops cleanly only at the run-time budget or limit); retry or split the query.
  • Also available as a hosted MCP tool (j0401--tx-esbd-search) for AI assistants.

Also available in this account:

  • TX Procurement Change Monitor - new and changed solicitations since your last run.
  • TX Contract Awards - who won, for how much, with HUB status and VPTS grades.
  • TX Vendor Directory (CMBL) - the supplier-side register with VID-level performance.
  • TX Business Entities - the franchise-tax / SOS register for those vendors.

License

MIT - see ./LICENSE.

Disclaimer

This product is an independent data service and is not affiliated with, endorsed by, or sponsored by the State of Texas or any government agency. All solicitation data is public government information. We recommend verifying any opportunity on the official portal before acting.