<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ben Barazandeh – Data Tools]]></title><description><![CDATA[Ben Barazandeh – Data Tools]]></description><link>https://dedicatedcrest.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 09 Nov 2026 11:55:15 GMT</lastBuildDate><atom:link href="https://dedicatedcrest.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Querying the Grants.gov API in Python: Open Federal Grants by Keyword and Eligibility]]></title><description><![CDATA[Originally published on Dev.to. Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. The tutorial…]]></description><link>https://dedicatedcrest.hashnode.dev/querying-the-grants-gov-api-in-python-open-federal-grants-by-keyword-and-eligibility</link><guid isPermaLink="true">https://dedicatedcrest.hashnode.dev/querying-the-grants-gov-api-in-python-open-federal-grants-by-keyword-and-eligibility</guid><dc:creator><![CDATA[Ben Barazandeh]]></dc:creator><pubDate>Sun, 08 Nov 2026 00:30:04 GMT</pubDate><content:encoded><![CDATA[<p>Originally published on <a href="https://dev.to/dedicated_crest/querying-the-grantsgov-api-in-python-open-federal-grants-by-keyword-and-eligibility-4b14">Dev.to</a>.</p>
<p><em>Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. The tutorial part needs only Python and the free Grants.gov API.</em></p>
<p>Every federal grant opportunity (NOFO) is posted on Grants.gov, and Grants.gov has a public JSON API with no key. Hardly anyone uses it directly, partly because the useful parts (award ceiling, total funding, eligibility, agency contact) aren't in the search results. Here's how to get them, and the quirks I hit.</p>
<h2>The two endpoints</h2>
<ul>
<li><code>POST https://api.grants.gov/v1/api/search2</code> returns matching opportunities: id, number, title, agency, status, open and close dates. Up to <code>rows</code> per page, with <code>startRecordNum</code> for paging.</li>
<li><code>POST https://api.grants.gov/v1/api/fetchOpportunity</code> returns one opportunity's full detail: the synopsis with award ceiling and floor, estimated total funding, expected awards, cost sharing, eligible applicant types, Assistance Listings (CFDA) and the contact.</li>
</ul>
<h2>Step 1: Search, then fetch details</h2>
<p>This ran as-is on 25 Oct 2026:</p>
<pre><code class="language-python">import requests
s = requests.post("https://api.grants.gov/v1/api/search2", json={
    "keyword": "artificial intelligence", "oppStatuses": "forecasted|posted", "rows": 3}, timeout=30).json()
print(s["data"]["hitCount"], "matches")
for hit in s["data"]["oppHits"]:
    d = requests.post("https://api.grants.gov/v1/api/fetchOpportunity",
                      json={"opportunityId": int(hit["id"])}, timeout=30).json()["data"]
    syn = d.get("synopsis") or {}
    print(hit["number"], hit["closeDate"] or "-", syn.get("awardCeiling"), syn.get("agencyContactEmail"))
</code></pre>
<p>Output (I've trimmed one individual's email):</p>
<pre><code>167 matches
PD-19-127Y 02/10/2027 none grantsgovsupport@nsf.gov
W519TC-25-S-0001 - None None
G27AS00079 11/14/2026 150000 &lt;agency contact&gt;
</code></pre>
<h2>Step 2: Filter by who can apply</h2>
<p>This is the filter that matters most for nonprofits, universities and small businesses. Grants.gov tags each opportunity with eligible applicant codes, and <code>search2</code> accepts them pipe-separated in <code>eligibilities</code>. A few: <code>12</code> = 501(c)(3) nonprofits, <code>13</code> = nonprofits without 501(c)(3), <code>20</code> = private institutions of higher education, <code>06</code> = public and state-controlled institutions of higher education, <code>23</code> = small businesses, <code>25</code> = others.</p>
<pre><code class="language-python">s = requests.post("https://api.grants.gov/v1/api/search2", json={
    "oppStatuses": "posted", "eligibilities": "12", "rows": 3}, timeout=30).json()["data"]
print(s["hitCount"])  # 440 on 25 Oct 2026
</code></pre>
<p>So on that day, 440 posted opportunities accepted 501(c)(3) applicants.</p>
<h2>Step 3: The quirks</h2>
<ul>
<li><strong>Inconsistent types.</strong> <code>awardCeiling</code> came back as the string <code>"none"</code> for the NSF program and as a number for the USGS one. A forecasted DoD notice had no synopsis at all (it lives in a different <code>forecast</code> block). Parse defensively and normalise to <code>null</code>.</li>
<li><strong>US dates.</strong> <code>closeDate</code> is <code>MM/DD/YYYY</code>, and blank for forecasts and rolling programs. Convert before sorting by "closing soon", or <code>01/07/2028</code> sorts before <code>11/17/2026</code>.</li>
<li><strong>Counts move daily.</strong> My keyword search had 171 matches on 18 Oct and 167 on 25 Oct. Opportunities close, and new ones are posted. Store the opportunity <code>id</code> and diff, rather than trusting counts.</li>
<li><strong>One detail call per result.</strong> Search is cheap, but anything useful needs <code>fetchOpportunity</code>. For 400+ results that's 400+ requests. Be gentle and cache.</li>
<li><strong>Contacts are often individuals.</strong> The agency contact is sometimes a shared inbox, sometimes a named program officer. Use it to ask about the program, not as a mailing list.</li>
</ul>
<h2>Step 4: Or use the packaged version</h2>
<p>I packaged this as an Apify Actor, <a href="https://apify.com/dedicated_crest/grants-gov-opportunity-finder">Grants.gov Scraper – Federal &amp; Nonprofit Grants Finder</a>. It runs the search, fetches every detail, normalises the types and dates, and returns one clean row per opportunity. It also adds <code>daysUntilClose</code>, and "posted in the last N days" / "closing within N days" filters for alerts.</p>
<p><strong>Run-ready link:</strong> <a href="https://console.apify.com/actors/imbcI522UkGM5yadr/input">open it in Apify Console with an example input already filled in</a> (<code>{"keyword": "artificial intelligence", "maxItems": 20}</code>). Click Start, or change the input first.</p>
<p>The pre-filled input (keyword <code>artificial intelligence</code>, forecasted + posted, 20 results) ran on 18 Oct 2026 in about 6 seconds and saved 20 of 171 matches:</p>
<table>
<thead>
<tr>
<th>Number</th>
<th>Title</th>
<th>Agency</th>
<th>Deadline</th>
<th>Award ceiling</th>
<th>Total funding</th>
</tr>
</thead>
<tbody><tr>
<td>26-512</td>
<td>Unlocking Dataset Value for AI-Enabled Scientific Discovery…</td>
<td>NSF</td>
<td>2026-11-04</td>
<td>–</td>
<td>$100M</td>
</tr>
<tr>
<td>25-531</td>
<td>Cybersecurity Innovation for Cyberinfrastructure (CICI)</td>
<td>NSF</td>
<td>2027-01-20</td>
<td>$1.2M</td>
<td>$12M</td>
</tr>
<tr>
<td>G27AS00079</td>
<td>Cooperative Agreement for Affiliated Partner with the Great…</td>
<td>DOI-USGS1</td>
<td>2026-11-14</td>
<td>$150K</td>
<td>$150K</td>
</tr>
</tbody></table>
<p>The nonprofit version:</p>
<pre><code class="language-json">{ "statuses": ["posted"], "eligibilities": ["12"], "sortBy": "closingSoon", "maxItems": 10 }
</code></pre>
<p>That ran on 19 Oct 2026 and saved 10 of 443 matching opportunities. One was HRSA-27-005, the FY2027 Health Center Service Area Competition, with an award ceiling of $10,116,100, a floor of $650,000, $267,773,700 total, 63 expected awards, and <code>daysUntilClose: 0</code>. Sorting by closing date surfaces exactly the ones you're about to miss.</p>
<p>Each row has: id, number, title, agency, status, open and close dates, days left, CFDA numbers, award ceiling and floor, estimated total funding, expected awards, cost sharing, funding instruments, categories, eligible applicants, description, contact and the grants.gov link.</p>
<h2>As a grant alert</h2>
<p>Schedule a weekly run with "posted in the last 7 days", your eligibility codes and keywords, and push the new rows to email, Slack or a sheet. That replaces scanning the Grants.gov site by hand.</p>
<h2>Limits, honestly</h2>
<ul>
<li><strong>Federal only.</strong> Grants.gov covers federal opportunities. Foundation, corporate and most state grants aren't there.</li>
<li><strong>It returns what Grants.gov publishes.</strong> If an agency leaves the ceiling or funding blank, so does the row. Forecasts usually have no deadline yet.</li>
<li><strong>Not a full NOFO.</strong> Eligibility rules, page limits and review criteria are in the attached documents, which you'll still need to read.</li>
<li><strong>Partial results.</strong> If Grants.gov errors partway through a large run, the Actor keeps what it saved and stops instead of failing the whole run.</li>
<li>Not affiliated with Grants.gov.</li>
</ul>
<h2>What it costs</h2>
<p><strong>$0.003 per opportunity saved</strong> ($3 per 1,000), no start fee, and it stops at your maximum cost per run. The 20-result test was 6 cents. If you only need one search, the Python above is free and works fine.</p>
<p>If you look for grants regularly: what do you filter on, and what's missing from Grants.gov's own search? I'd like to know what to add next, so tell me in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Building Healthcare Provider Lists From the Free NPI Registry (and Getting Past Its 1,200-Result Cap)]]></title><description><![CDATA[This is a copy of my Dev.to post. Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in…]]></description><link>https://dedicatedcrest.hashnode.dev/building-healthcare-provider-lists-from-the-free-npi-registry-and-getting-past-its-1-200-result-cap</link><guid isPermaLink="true">https://dedicatedcrest.hashnode.dev/building-healthcare-provider-lists-from-the-free-npi-registry-and-getting-past-its-1-200-result-cap</guid><category><![CDATA[healthcare]]></category><category><![CDATA[api]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Ben Barazandeh]]></dc:creator><pubDate>Fri, 06 Nov 2026 19:42:22 GMT</pubDate><content:encoded><![CDATA[<p><em>This is a copy of my <a href="https://dev.to/dedicated_crest/building-healthcare-provider-lists-from-the-free-npi-registry-and-getting-past-its-1200-result-8a">Dev.to post</a>.</em></p>
<p><em>Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.</em></p>
<p>If you sell to dentists, clinics, chiropractors or therapists, or you need to verify a list of providers, the best free source in the US is the <strong>NPI Registry</strong> (NPPES), run by CMS. Every HIPAA-covered provider has a National Provider Identifier, about 8 million of them, and the registry has a public API with no key.</p>
<p>Each record has the NPI, name, credential, taxonomies (specialties), license, practice and mailing addresses with <strong>phone and fax</strong>, the date the NPI was issued, and for organizations the <strong>authorized official</strong> (often the owner or CEO) with title and phone.</p>
<p>What it doesn't have: <strong>email addresses</strong>. Plan around that from the start.</p>
<p>If you need <em>everything</em>, CMS also publishes the full NPPES data dissemination file (monthly, plus weekly updates). It's free and complete, and it's a very large CSV with hundreds of columns. For targeted lists, the API is easier.</p>
<h2>Step 1: Query the API</h2>
<pre><code>https://npiregistry.cms.hhs.gov/api/?version=2.1&amp;taxonomy_description=Dentist&amp;city=San Diego&amp;state=CA&amp;limit=200
</code></pre>
<p><code>limit</code> is at most 200, and you page with <code>skip</code>. Here's a pager that also keeps only providers whose <em>primary</em> taxonomy is a dentist one. It ran as-is on 25 Oct 2026:</p>
<pre><code class="language-python">import requests
def npi(**q):
    out, skip = [], 0
    while True:
        r = requests.get("https://npiregistry.cms.hhs.gov/api/", params={
            "version": "2.1", "limit": 200, "skip": skip, **q}, timeout=30).json()
        batch = r.get("results", [])
        out += batch
        if len(batch) &lt; 200 or skip &gt;= 1000:   # skip is capped at 1000 -&gt; max 1,200 per query
            return out
        skip += 200
rows = npi(taxonomy_description="Dentist", city="San Diego", state="CA")
print(len(rows))
primary = [r for r in rows if any(t["primary"] and "Dent" in t["desc"] for t in r["taxonomies"])]
print(len(primary), "with a dentist taxonomy as primary")
</code></pre>
<p>Output:</p>
<pre><code>1200
1193 with a dentist taxonomy as primary
</code></pre>
<p>Exactly 1,200 is suspicious, and it should be.</p>
<h2>Step 2: The pitfalls</h2>
<ul>
<li><strong>Every query stops at 1,200 results.</strong> <code>skip</code> is capped at 1000, and a page holds 200. Worse, it fails silently: I asked for <code>skip=1000</code>, <code>skip=1200</code> and <code>skip=2000</code> on a California dentist search, and all three returned the same page. If you just keep paging, you collect duplicates and think you're done. To get a whole city or state, split the search (by city, or by ZIP prefix, and for a state by both) until each piece is under 1,200, then dedupe on NPI.</li>
<li><strong>Specialty matches <em>any</em> taxonomy.</strong> A provider with Dentist as a secondary taxonomy still matches. Above, 7 of 1,200 didn't have a dentist taxonomy as primary. For strict lists, check the <code>primary</code> flag.</li>
<li><strong>Two addresses.</strong> Each record has a <code>MAILING</code> and a <code>LOCATION</code> address, and in the record I checked the mailing address came first in the list. Use <code>address_purpose</code>, not position. City and state searches can match either one, so a "San Diego" search can include providers who only <em>mail</em> from there.</li>
<li><strong>"New practices" isn't a query parameter.</strong> The API can't filter on the NPI issue date (<code>enumeration_date</code>). To find newly enumerated providers, you pull and filter client-side.</li>
<li><strong>Self-reported data.</strong> Phones and fax numbers are what providers entered, and some records haven't been updated in years. <code>last_updated</code> tells you how stale a record is.</li>
</ul>
<h2>Step 3: Or use the packaged version</h2>
<p>I packaged this as an Apify Actor, <a href="https://apify.com/dedicated_crest/npi-healthcare-provider-leads">NPI Registry Scraper – NPPES Healthcare Provider Leads</a>. The main thing it adds is <strong>auto-splitting</strong>: large searches are split by state and ZIP prefix, so you can pull a whole state past the 1,200 cap. When it splits by state, it keeps providers whose <em>practice</em> location is in that state.</p>
<p><strong>Run-ready link:</strong> <a href="https://console.apify.com/actors/AeZTbhypXKAXC4S18/input">open it in Apify Console with an example input already filled in</a> (<code>{"specialties": ["Dentist"], "states": ["CA"], "cities": ["San Diego, CA"], "maxItems": 50}</code>). Click Start, or change the input first.</p>
<pre><code class="language-json">{
  "specialties": ["Dentist"],
  "states": ["CA"],
  "cities": ["San Diego, CA"],
  "maxItems": 50
}
</code></pre>
<p>Other inputs: individual vs organization, ZIPs, names, a list of NPIs to verify, and filters for <strong>NPI issued after</strong>, record updated after, only with phone, and primary specialty must match. Output is deduplicated, with formatted phones and ZIPs and separate practice and mailing addresses.</p>
<p>That input ran on 18 Oct 2026 in about 18 seconds and returned 50 providers. Some rows:</p>
<table>
<thead>
<tr>
<th>NPI</th>
<th>Name</th>
<th>Type</th>
<th>Primary specialty</th>
<th>NPI issued</th>
</tr>
</thead>
<tbody><tr>
<td>1326171653</td>
<td>Dr. WILLIAM JOSEPH DI ZINNO, DMD</td>
<td>Individual</td>
<td>Dentist, General Practice</td>
<td>2007-03-14</td>
</tr>
<tr>
<td>1699214478</td>
<td>GLENN C. ASHMORE DDS, INC.</td>
<td>Organization</td>
<td>Dentist</td>
<td>2017-02-14</td>
</tr>
<tr>
<td>1730639188</td>
<td>A.S. KIM DDS, INC.</td>
<td>Organization</td>
<td>Dentist, General Practice</td>
<td>2016-10-10</td>
</tr>
<tr>
<td>1487251294</td>
<td>Dr. ZEKI ABDULAALI, DDS</td>
<td>Individual</td>
<td>Dentist, General Practice</td>
<td>2020-10-05</td>
</tr>
</tbody></table>
<p>Organization rows are the useful ones for B2B. For <code>A.S. KIM DDS, INC.</code>, the row has the authorized official (a DDS, title "CEO") with a phone, the practice phone, license number, other names (DBA "VILLAGE HILLCREST DENTAL"), and the count of additional practice locations.</p>
<p>A bigger real example from my testing: all individual chiropractors in California with a phone came to 3,000 providers in about 3 minutes.</p>
<h2>A monthly "new practices" list</h2>
<p>Set <em>NPI issued after</em> (<code>enumeratedAfter</code>) to 90 days ago, pick your specialties and states, and schedule it monthly. New clinicians and new group practices show up as they get their NPIs.</p>
<h2>Limits, honestly</h2>
<ul>
<li><strong>No emails.</strong> NPPES doesn't publish them. You get practice phone and fax, the mailing address and, for organizations, the authorized official. If you need an email, you'll need the practice's website.</li>
<li><strong>Self-reported and sometimes stale.</strong></li>
<li><strong>NPI holders only.</strong> It covers HIPAA-covered providers, not every wellness or cosmetic business.</li>
<li>Not affiliated with CMS or HHS.</li>
</ul>
<h2>What it costs</h2>
<p><strong>$0.001 per provider</strong> ($1 per 1,000), no start fee. Duplicates and filtered-out records are free, and runs stop at your "maximum cost per run". The 50-provider test was 5 cents. The API and the full CMS file are free.</p>
<h2>Play fair</h2>
<p>Practice phones are business lines, but authorized officials are people. TCPA rules apply to calls and texts, CAN-SPAM to email, and some providers will rightly be annoyed by cold outreach. Be relevant and honour opt-outs.</p>
<p>Do you work with NPPES data? I'd like to hear how you deal with stale records, so tell me in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Trucking Company List From FMCSA Open Data (SoQL Pitfalls Included)]]></title><description><![CDATA[This is a copy of my Dev.to post. Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. The tutorial…]]></description><link>https://dedicatedcrest.hashnode.dev/building-a-trucking-company-list-from-fmcsa-open-data-soql-pitfalls-included</link><guid isPermaLink="true">https://dedicatedcrest.hashnode.dev/building-a-trucking-company-list-from-fmcsa-open-data-soql-pitfalls-included</guid><category><![CDATA[logistics]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[SQL]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Ben Barazandeh]]></dc:creator><pubDate>Fri, 06 Nov 2026 17:11:59 GMT</pubDate><content:encoded><![CDATA[<p><em>This is a copy of my</em> <a href="https://dev.to/dedicated_crest/building-a-trucking-company-list-from-fmcsa-open-data-soql-pitfalls-included-4jo9"><em>Dev.to post</em></a><em>.</em></p>
<p><em>Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. The tutorial part uses only free public data.</em></p>
<p>Freight insurance agents, factoring companies, ELD and fuel-card sellers, and brokers looking for capacity all need the same thing: a list of trucking companies with a phone number and fleet size. People pay for those lists, but the source behind most of them is free. Every US interstate carrier files an <strong>MCS-150</strong> with FMCSA, and US DOT publishes the result as open data, refreshed daily.</p>
<p>Here's how to query it yourself, and what tripped me up.</p>
<h2>The datasets</h2>
<p>All on <a href="http://data.transportation.gov">data.transportation.gov</a> (Socrata), no key needed for light use:</p>
<ul>
<li><strong>Company Census File</strong> (<code>az4n-8mr2</code>): one row per USDOT number, with legal name, address, phone, email (if filed), power units, drivers, cargo, operation type and status.</li>
<li><strong>L&amp;I "Carrier – All With History"</strong> (<code>6eyk-hxee</code>): MC dockets, authority and insurance.</li>
<li><strong>L&amp;I "AuthHist – All With History"</strong> (<code>9mw4-x3tu</code>): authority grants, revocations and reinstatements.</li>
</ul>
<h2>Step 1: A SoQL query</h2>
<p>Small Dallas carriers (5–50 power units), active, with an email on file:</p>
<pre><code class="language-bash">curl -s -G "https://data.transportation.gov/resource/az4n-8mr2.json" \
  --data-urlencode "\$select=dot_number,legal_name,phone,email_address,power_units,total_drivers" \
  --data-urlencode "\$where=phy_state='TX' AND phy_city='DALLAS' AND status_code='A' AND power_units ::number BETWEEN 5 AND 50 AND email_address IS NOT NULL" \
  --data-urlencode "\$order=dot_number" \
  --data-urlencode "\$limit=5"
</code></pre>
<p>Run on 25 Oct 2026 (emails left out here):</p>
<table>
<thead>
<tr>
<th>dot_number</th>
<th>legal_name</th>
<th>phone</th>
<th>power_units</th>
<th>total_drivers</th>
</tr>
</thead>
<tbody><tr>
<td>58702</td>
<td>ECOBAT LOGISTICS US INC</td>
<td>2145830296</td>
<td>46</td>
<td>20</td>
</tr>
<tr>
<td>59903</td>
<td>BISHOP INTERNATIONAL INC</td>
<td>2144266449</td>
<td>10</td>
<td>6</td>
</tr>
<tr>
<td>60111</td>
<td>DAVIS-HAWN LUMBER COMPANY</td>
<td>2149460734</td>
<td>8</td>
<td>5</td>
</tr>
<tr>
<td>109485</td>
<td>TEXAS INSTRUMENTS INCORPORATED</td>
<td>2145674884</td>
<td>11</td>
<td>4</td>
</tr>
<tr>
<td>164404</td>
<td>WALNUT HILL WRECKER SERVICEINC</td>
<td>9726202160</td>
<td>17</td>
<td>14</td>
</tr>
</tbody></table>
<h2>Step 2: The pitfalls</h2>
<p><strong>Numbers are stored as text.</strong> My first version used <code>power_units BETWEEN 5 AND 50</code> and Socrata rejected it with a type mismatch. You need the <code>::number</code> cast. Without a cast, a comparison like <code>&gt;</code> on a text column runs as a string comparison, so <code>"9" &gt; "50"</code> is true.</p>
<p><strong>Cities are free text, uppercase.</strong> <code>phy_city='DALLAS'</code> works, but a filer who typed a suburb or a typo won't match. For full coverage, filter by ZIP prefix (<code>phy_zip like '752%'</code>) as well as city.</p>
<p><strong>"Active" isn't one thing.</strong> <code>status_code='A'</code> is the USDOT status. Whether a company can legally haul freight for hire is a separate question, the <strong>operating authority</strong> (MC number), and that lives in the L&amp;I datasets. The census also has private fleets: Texas Instruments and a lumber company are in my list because they run their own trucks. If you're selling to for-hire carriers, filter on the classification and join authority status.</p>
<p><strong>The join is the slow part.</strong> Authority history is per docket, with multiple grant, revocation and reinstatement rows. You have to fetch the dockets for your candidate USDOT numbers and work out the current state. My Actor's test run (below) used the same city and fleet filters plus "active authority", and it left out Bishop International and Davis-Hawn Lumber, which my raw query includes. So the census alone and census + authority give different lists.</p>
<p><strong>Formatting.</strong> Phones come back as raw digits, and names like <code>SERVICEINC</code> are as filed. Email exists only when the company put one on its MCS-150.</p>
<h2>Step 3: Or use the packaged version</h2>
<p>I packaged this as an Apify Actor, <a href="https://apify.com/dedicated_crest/fmcsa-trucking-carrier-leads">FMCSA Carrier Leads – Trucking Companies &amp; Freight Brokers List</a>. It builds the SoQL for you, joins the census with both L&amp;I datasets, and returns one clean row per company.</p>
<p><strong>Run-ready link:</strong> <a href="https://console.apify.com/actors/OYGIEL4x0GmbUh4rc/input">open it in Apify Console with an example input already filled in</a> (<code>{"states": ["TX"], "cities": ["Dallas, TX"], "maxItems": 25}</code>). Click Start, or change the input first.</p>
<pre><code class="language-json">{"states":["TX"],"cities":["Dallas, TX"],"entityType":"carrier","minPowerUnits":5,"maxPowerUnits":50,"authorityStatus":"active","requireEmail":true,"maxItems":10}
</code></pre>
<p>Filters: states, cities, ZIP prefixes, USDOT or MC numbers, name contains, entity type (carrier, broker, both, freight forwarder, shipper), operation type, classification (for-hire, private…), USDOT status, authority status, power units and driver ranges, cargo types, hazmat, safety rating, USDOT add date, and "only with email".</p>
<p>That input ran on Apify on 22 Oct 2026 and returned 10 carriers. A few rows (emails omitted):</p>
<table>
<thead>
<tr>
<th>USDOT</th>
<th>MC</th>
<th>Legal name</th>
<th>Phone</th>
<th>Power units</th>
<th>Safety rating</th>
<th>First authority</th>
</tr>
</thead>
<tbody><tr>
<td>58702</td>
<td>MC1250042</td>
<td>ECOBAT LOGISTICS US INC</td>
<td>(214) 583-0296</td>
<td>46</td>
<td>Satisfactory</td>
<td>2021-06-10</td>
</tr>
<tr>
<td>164404</td>
<td>MC142549</td>
<td>WALNUT HILL WRECKER SERVICEINC</td>
<td>(972) 620-2160</td>
<td>17</td>
<td>Satisfactory</td>
<td>1978-01-25</td>
</tr>
<tr>
<td>275346</td>
<td>MC193564</td>
<td>JACK KEY AUTO TRANSPORT INC</td>
<td>(972) 557-8210</td>
<td>15</td>
<td>Satisfactory</td>
<td>1990-12-21</td>
</tr>
</tbody></table>
<p>Each record has formatted phones, the full address, company officers, fleet and driver counts, MCS-150 mileage, cargo types, safety rating and review dates, common/contract/broker authority, pending and revocation flags, BIPD insurance on file vs required, and the authority history. For Ecobat, the history shows the authority <strong>revoked on 2022-12-19 and reinstated on 2022-12-23</strong>, the kind of detail that matters if you're vetting. Every record also links to the SAFER snapshot. A <code>SUMMARY</code> record shows records scanned, API calls and the exact SoQL filter used, so you can reproduce it.</p>
<h2>Recipes</h2>
<ul>
<li><strong>New carriers for insurance/factoring</strong>: USDOT added in the last 90 days, for-hire, 1–10 power units, your states, with email. Schedule it weekly.</li>
<li><strong>Capacity sourcing</strong>: cargo type "Refrigerated Food", 5–50 power units, Satisfactory rating, active authority.</li>
<li><strong>Broker list</strong>: entity type broker, active broker authority, by state.</li>
</ul>
<h2>Limits, honestly</h2>
<ul>
<li><strong>It's only as good as the MCS-150.</strong> Small carriers often file infrequently, so phones, emails and fleet sizes can be stale. Each record has <code>mcs150Date</code> so you can judge.</li>
<li><strong>Email is often missing.</strong> "Only with email" shrinks lists a lot.</li>
<li><strong>The authority filter is slow</strong> because it runs after the census query. Narrow by state, city or fleet size for big searches.</li>
<li><strong>No inspection or crash detail (SMS/CSA scores).</strong> Only the safety rating and review dates from the census.</li>
<li><strong>Compliance is on you.</strong> It's public government data, but TCPA and CAN-SPAM still apply when you call or email.</li>
</ul>
<h2>What it costs</h2>
<p><strong>$0.002 per company saved</strong> ($2 per 1,000). No start fee, and the run stops cleanly at your maximum charge. The 10-row test cost 2 cents. For a one-off pull of one city, the curl above is free.</p>
<p>If you work in freight: what do you actually filter on when you build a carrier list? Fleet size, cargo, authority age? I'd like to know which filters matter in practice, so tell me in the comments.</p>
]]></content:encoded></item></channel></rss>