# ThomasNet Scraper - Industrial Suppliers, Phones & Certs (`lanternlab318/thomasnet`) Actor

Search ThomasNet for North American industrial suppliers: company name, website, public phone, city/state, company type, annual sales, employees, year founded, product categories and public certification totals. No login. Deduplicated across terms; empty or cut-short searches are free.

- **URL**: https://apify.com/lanternlab318/thomasnet.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Lead generation, Business
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.99 / 1,000 suppliers

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ThomasNet Supplier Search — Industrial Suppliers with Firmographics

A Lantern Lab Actor. Give it product or service terms — "cnc machining", "injection molding", "stainless steel tubing" — or ThomasNet supplier profile URLs, and get thomasnet.com's public supplier listings back with the firmographics a sourcing list needs: **company name**, **ThomasNet profile link**, **own website**, **public phone**, **city / state / country**, **description**, **company type** (custom manufacturer, distributor, service company…), **annual sales**, **employees**, **year founded**, **product categories** and the **certification totals** the profile shows publicly. No ThomasNet login, no cookies, no account of your own.

**Price: USD 3.99 per 1,000 supplier rows. No run-start fee.** Terms that return nothing, per-input status rows and the run summary are free — you pay for suppliers, not for searches.

### What you get

- **Every supplier the public search shows, labelled with the term that found it.** Each term is searched separately, in the order you gave it, and every row carries the term and the rank ThomasNet gave that supplier in it.
- **Deduplicated across the whole run.** The key is ThomasNet's own supplier id, so a company that matches several of your terms — or that you also listed as a profile URL — is written once and charged once; the input's status row says how many were skipped as duplicates.
- **Firmographics, not just links.** Company type comes from ThomasNet's own facet (labels like "Custom Manufacturer; Manufacturer", never guessed from the name), alongside annual sales range, employee range, founding year and the product categories the supplier is filed under. Fields ThomasNet did not send are `null`, never guesses.
- **Coverage by state or province.** The optional `region` takes ThomasNet's own "Locations Served" coverage codes — the same filter its state list uses, split states included (`CN`/`CS` for northern/southern California, `ON`/`QC`… for Canada). Leave it empty for all of North America.
- **Certifications, honestly.** ThomasNet lists certificate *names* only to signed-in accounts; the public profile shows totals. So the `certifications` field carries those public totals (for example `"Quality Certifications: 12"`), and this README is where you read that the names are behind ThomasNet's login, not dropped by accident.
- **Empty is never reported as failure, and short is never reported as empty.** A term ThomasNet answers with no suppliers gets a plain "empty result" status row. A term cut short by your limit, the request ceiling or a refusal from ThomasNet is `partial: true` with the reason — free of charge either way.
- **It caps itself.** `maxUpstreamRequests` is a hard ceiling on the requests a run sends to ThomasNet, worked out from your input when you leave it empty, so a run is never unbounded.

### What it does not do

- **One page per search term — that is everything ThomasNet's public search gives.** Without signing in, thomasnet.com serves the first results page only (25 suppliers, measured 2026-10-23) and answers page 2 with a registration wall. A term with more matches than that is marked `partial: "platform_page_limit"` with the total the page itself reports, so the missing suppliers are visible as missing, never silently dropped. This Actor does not sign in and does not go around the wall.
- **No certification names.** Public totals only, as above.
- **No personal contact data.** No contact-person names, no e-mail addresses, no RFQ, nothing that needs a ThomasNet account. The `phone` field is the company's public listing phone only.
- No image or CAD-file downloads, no supplier catalogs, no review or ranking data beyond what the search itself orders by.
- No scheduling or monitoring built in; use Apify schedules.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `searchTerms` | — | One product/service term per line. Every term is searched separately. Give at least one of `searchTerms` / `supplierUrls`. |
| `supplierUrls` | — | ThomasNet supplier profile URLs (or bare `/company/.../profile` paths), one per line, collected in full from their public profiles. |
| `region` | `NA` | ThomasNet "Locations Served" coverage code: a state (split states `CN`/`CS`, `NO`/`SO`, `JN`/`JS`, `DN`/`UN`, `EM`/`WM`, `EP`/`WP`, `NT`/`GT`), a Canadian province (`ON`, `QC`, `BC`, `AB`, …) or empty for all of North America. Filters by where suppliers deliver, not only where they are headquartered. |
| `maxSuppliersPerTerm` | `50` | Upper bound on the rows one search term may produce. The anonymous page holds 25, so anything up to 50 changes nothing unless a term's page happens to be fuller. |
| `includeDetails` | `true` | Also fetch each supplier's profile to fill `certifications` with the public totals. When `false`, `certifications` stays `null` and a term costs one request instead of one per supplier. |
| `maxUpstreamRequests` | derived | Hard ceiling on HTTP requests to ThomasNet for the whole run. Derived from your input when empty. |
| `proxyConfiguration` | Apify Proxy | Datacenter by default, which is what this Actor is measured on; thomasnet.com serves its public search and profile pages over it without a bot wall. |

```json
{
  "searchTerms": ["cnc machining", "injection molding"],
  "region": "CN",
  "maxSuppliersPerTerm": 25,
  "includeDetails": true
}
```

### Output

One row per supplier, `recordType: "supplier"`:

```json
{
  "recordType": "supplier",
  "supplierId": "400300",
  "name": "Eagle Stainless Tube & Fabrication, Inc.",
  "url": "https://www.thomasnet.com/company/eagle-stainless-tube-fabrication-inc-400300/profile",
  "website": "https://www.eaglestainless.com/",
  "phone": "(508) 528-9801",
  "city": "Franklin",
  "state": "MA",
  "country": "USA",
  "description": "Manufacturer of standard and custom stainless steel tubing...",
  "supplierType": "Manufacturer",
  "annualSales": "$10 - 24.9 Mil",
  "employees": "50-99",
  "yearFounded": "1981",
  "certifications": ["Quality Certifications: 12"],
  "productCategories": ["CNC Machining", "Stainless Steel Tubing"],
  "sourceType": "search",
  "searchTerm": "cnc machining",
  "sourceUrl": null,
  "rank": 1,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-23T14:05:00.000Z"
}
```

Every search term and every profile URL also gets one uncharged `source_status` row:

```json
{
  "recordType": "source_status",
  "sourceType": "search",
  "searchTerm": "cnc machining",
  "sourceUrl": null,
  "suppliersReturned": 25,
  "suppliersCharged": 25,
  "duplicatesSkipped": 0,
  "suppliersAvailable": 29430,
  "partial": true,
  "partialReason": "platform_page_limit",
  "statusText": "ThomasNet serves the first results page (25 suppliers) without an account...",
  "collectedAt": "2026-10-23T14:05:00.000Z"
}
```

The last row is `recordType: "RUN_SUMMARY"`: totals for the run — suppliers collected and charged, duplicates skipped, empty and partial inputs by reason, upstream requests sent and refused, and why the run stopped if it stopped early.

#### Field reference — supplier rows

| Field | Meaning |
| --- | --- |
| `supplierId` | ThomasNet's own supplier id (the `tgramsId` the site keys profiles by). |
| `name`, `url`, `website`, `phone` | Company name, ThomasNet profile URL, the supplier's own website, and the public listing phone. |
| `city`, `state`, `country` | Address as ThomasNet prints it. |
| `description` | The company blurb from the listing. |
| `supplierType` | ThomasNet's company-type labels, primary first (`Manufacturer`, `Custom Manufacturer`, `Distributor`, …), `;`-joined. `null` when the listing carries none. |
| `annualSales`, `employees`, `yearFounded` | The ranges/values the listing states, verbatim. `null` when not stated. |
| `certifications` | Public certification totals from the profile (`"Quality Certifications: 12"`). `null` when `includeDetails` is off or the detail request failed — see `partialReason`. |
| `productCategories` | The headings ThomasNet files the supplier under, in the spelling its result card shows. |
| `sourceType` / `searchTerm` / `sourceUrl` | What produced the row: a search term or one of your profile URLs. |
| `rank` | Position in that input's results, from 1. A gap in one input's ranks is a supplier an earlier input had already collected. |
| `partial`, `partialReason` | For a supplier row, `partial` only ever means its detail request failed (`detail_error`) and `certifications` is `null`. |
| `collectedAt` | When this Actor observed the row (ISO 8601). |

### Limits

- **25 suppliers per search term.** ThomasNet's anonymous search serves one page; the wall after it is the site's, and the status row says so with the total match count when the page reports one. If you need deeper coverage per term, run several narrower terms (by region or by product wording) rather than expecting pagination.
- **Certification totals, not names.** Certificate names are inside the report a ThomasNet account requests; this Actor collects only what the logged-out profile shows.
- **Company data only.** No contact-person names, e-mails or RFQ traffic; the phone is the company's public listing phone.
- Runs are read-only against thomasnet.com: public pages, no login, no cookies, nothing posted.

# Actor input Schema

## `searchTerms` (type: `array`):

One ThomasNet search term per line: "cnc machining", "injection molding", "stainless steel tubing". Each term is searched separately and its suppliers are labelled with it. A supplier found by several of your terms is returned once and charged once.

## `supplierUrls` (type: `array`):

One ThomasNet supplier profile per line: "https://www.thomasnet.com/company/eagle-stainless-tube-fabrication-inc-400300/profile". The bare /company/... path also works. Each profile's public firmographics are returned as a supplier row. A link that resolves to nobody is reported as such, not as an empty result.

## `region` (type: `string`):

Optional ThomasNet "Locations Served" coverage code, the same filter the site's own state list uses. Big states are split: California is CN (north) / CS (south), Ohio is NO / SO, New Jersey JN / JS, New York DN (metro) / UN (upstate), Massachusetts EM / WM, Pennsylvania EP / WP, Texas NT / GT. Canadian provinces use their own codes (ON, QC, BC, AB, MB, SK, NS, NB, NL, PE, NU, YT). Leave empty for all of North America (cov=NA). This filters by where suppliers deliver, not only where they are headquartered; each row still carries the supplier's own address.

## `maxSuppliersPerTerm` (type: `integer`):

Upper bound on the rows one search term may produce. Without signing in ThomasNet serves the first results page only - 25 suppliers per term - and asks for a (free) account beyond that; this Actor does not sign in, so a term with more matches than that is marked partial with the count it did collect, never reported as complete or as empty. Supplier URL inputs are not affected by this limit.

## `includeDetails` (type: `boolean`):

When on (default), each collected supplier's profile page is also fetched to fill the certifications field with the certification totals ThomasNet shows publicly (for example "Quality Certifications: 12"). Certification names are only shown to signed-in accounts, so this Actor returns the public totals, not the names. When off, certifications stays null and the run sends one request per search term instead of one per supplier.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to ThomasNet: search pages and supplier profiles together. At the ceiling collection stops - the inputs not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your terms, URLs and maxSuppliersPerTerm, so a run is never unbounded.

## `proxyConfiguration` (type: `object`):

Where the requests to ThomasNet come from. The default (Apify Proxy, datacenter) is the cheapest and is what this Actor is measured on; thomasnet.com serves its public search results and supplier profiles over it without a login. Set a proxy country only if you want to compare ThomasNet's country-specific behaviour.

## Actor input object example

```json
{
  "searchTerms": [
    "cnc machining"
  ],
  "supplierUrls": [],
  "maxSuppliersPerTerm": 50,
  "includeDetails": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "cnc machining"
    ],
    "supplierUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/thomasnet").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchTerms": ["cnc machining"],
    "supplierUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/thomasnet").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "cnc machining"
  ],
  "supplierUrls": []
}' |
apify call lanternlab318/thomasnet --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lanternlab318/thomasnet"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/wbMRXJeZcaBwQrWTp/builds/bhHojJUL3Mnw4dSVW/openapi.json
