# DOTmed Medical Equipment Scraper (`subrosa/dotmed-equipment`) Actor

Scrape used, refurbished, and new medical equipment listings and auctions from DOTmed.com: manufacturer, model, condition, price, current bid, seller, and photos. Imaging, dental, surgical, cardiology, and lab equipment. Structured JSON for procurement and resale pricing.

- **URL**: https://apify.com/subrosa/dotmed-equipment.md
- **Developed by:** [Sub Rosa](https://apify.com/subrosa) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 results

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

## DOTmed Equipment Listings

Structured medical equipment listings and auctions from DOTmed.com, the
largest online marketplace for used, refurbished, and new medical
equipment. Each item returned is one listing or one live auction, with
its category, manufacturer, model, condition, price or current bid,
seller's business name, description, and photos — normalized into a
consistent schema instead of the raw HTML a human visitor would have to
read page by page.

### Who uses this and for what

**Hospital procurement teams** comparing prices across vendors before a
capital equipment purchase — pulling every currently-listed CT scanner
or ultrasound system to see the range of asking prices and conditions
before making an offer.

**Medical equipment dealers and brokers** tracking available inventory
and sourcing opportunities across manufacturers — running this daily
against a category to catch new listings from a specific manufacturer
as soon as they post, rather than checking the site by hand.

**Refurbishment companies** looking for equipment to acquire, refurbish,
and resell, where the gap between an auction's current bid and a
comparable fixed listing's asking price is the margin they're pricing
against.

### Sample output

One real item from a real run (2026-09-26):

```json
{
  "id": "5620515",
  "url": "https://www.dotmed.com/listing/cabinetry-furnishings/kewaunee-scientific/int-2000/class-ii-a2-biological-safety-cabinet/5620515",
  "sourceUrl": "https://www.dotmed.com/browse/equipment/dental/all",
  "scrapedAt": "2026-09-26T19:09:58.000Z",
  "category": "dental",
  "subcategory": "Cabinetry - Furnishings",
  "manufacturer": "KEWAUNEE SCIENTIFIC",
  "model": "INT-2000",
  "title": "For Sale KEWAUNEE SCIENTIFIC INT-2000 CLASS II A2 BIOLOGICAL SAFETY CABINET",
  "condition": "Used",
  "priceUsd": 750,
  "priceCurrency": "USD",
  "priceText": "$750.00 USD",
  "description": "...Condition Good: Powers On Our Notes UV light, fan, light, receptacles all work. Untested for full functionality",
  "specifications": null,
  "sellerCompany": "Tekyard LLC",
  "sellerLocation": null,
  "sellerType": null,
  "listingType": "listing",
  "currentBidUsd": null,
  "bidCount": null,
  "auctionEndsAt": null,
  "imageUrls": ["https://images.dotmed.com/cgi-bin/size.pl?i=5620515.jpg&s=640&t=2"],
  "inStock": true,
  "listedAt": null
}
```

See `sample.json` for 13 real items from a real run, spanning imaging,
cardiology, dental, and surgical categories, both auctions and fixed
listings.

### Data dictionary

| Field | Type | Meaning | Example |
|---|---|---|---|
| `id` | string | DOTmed's own listing or auction number | `"5620515"` |
| `url` | string | Canonical URL of the item's own page | `"https://www.dotmed.com/listing/.../5620515"` |
| `sourceUrl` | string | The category page this item was reached from | `"https://www.dotmed.com/browse/equipment/dental/all"` |
| `scrapedAt` | string (ISO 8601, UTC) | When this item was fetched | `"2026-09-26T19:09:58.000Z"` |
| `category` | string | Top-level category as given in Actor input | `"dental"` |
| `subcategory` | string | null | Equipment type, DOTmed's own label | `"Cabinetry - Furnishings"` |
| `manufacturer` | string | null | Equipment manufacturer | `"KEWAUNEE SCIENTIFIC"` |
| `model` | string | null | Model name/number | `"INT-2000"` |
| `title` | string | null | The listing's own headline | `"For Sale KEWAUNEE SCIENTIFIC..."` |
| `condition` | string | null | `"New"`, `"Used"`, `"Refurbished"`, `"For Parts/Repair"` | `"Used"` |
| `priceUsd` | number | null | Fixed asking price. Null for auctions and "Contact for Price" listings | `750` |
| `priceCurrency` | string | Always `"USD"` | `"USD"` |
| `priceText` | string | null | Human-readable price, derived from `priceUsd` | `"$750.00 USD"` |
| `description` | string | null | Seller's own notes, boilerplate and contact info stripped | see above |
| `specifications` | null | Reserved — not populated in this version | `null` |
| `sellerCompany` | string | null | Seller's business name — never a person's name | `"Tekyard LLC"` |
| `sellerLocation` | null | Reserved — not populated in this version | `null` |
| `sellerType` | null | Reserved — not populated in this version | `null` |
| `listingType` | string | `"listing"` (fixed price) or `"auction"` | `"listing"` |
| `currentBidUsd` | number | null | Current highest bid. Only set for auctions | `11000` |
| `bidCount` | integer | null | Number of bids so far. Only set for auctions | `2` |
| `auctionEndsAt` | null | Reserved — not populated in this version | `null` |
| `imageUrls` | array of strings | This item's own photos | `["https://images.dotmed.com/..."]` |
| `inStock` | boolean | null | Whether DOTmed marks it currently available | `true` |
| `listedAt` | null | Reserved — not populated in this version | `null` |

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `categories` | array of strings | all 11 slugs below | Top-level DOTmed category slugs to crawl: `imaging`, `cardiology`, `dental`, `surgical`, `laboratory`, `endoscopy`, `respiratory`, `neonatal`, `homecare-rehab`, `veterinary`, `ophthalmology` — each verified live 2026-09-26. An invalid slug returns 0 items, not an error, so a typo fails quietly; stick to this list. |
| `manufacturers` | array of strings | `[]` | Optional post-extraction filter — keep only items whose manufacturer name contains one of these (case-insensitive). Empty means no filtering. |
| `maxItems` | integer | `30` | Stop once this many items have been delivered. |
| `scrapeDetails` | boolean | `true` | Visit each item's own page for full data. When off, only category-page fields are returned (thinner, faster). |
| `includeAuctions` | boolean | `true` | Include auction items. |
| `includeListings` | boolean | `true` | Include fixed-price items. |
| `proxyConfiguration` | object | Apify datacenter proxies | Standard Apify proxy editor. The house never uses residential proxies by default. |

### Pricing

Pay-per-event. Two events:

- **Actor start** — $0.02, charged once when the run begins.
- **Result** — $0.003 per item delivered to the dataset ($3 per 1,000
  items).

A run with the default input (4 categories, `maxItems: 30`,
`scrapeDetails: true`) costs about $0.02 + 30 × $0.003 = **$0.11** and
finishes in under two minutes.

### Limits and honesty

- **No proxy by default, on purpose.** DOTmed returns a fast HTTP 200
  but silently strips the embedded product data when a request comes
  from Apify's shared datacenter proxy pool — confirmed by running the
  same input with and without the proxy on the real platform. A direct
  connection under the same declared identity gets full data every
  time. The default input therefore uses no proxy. If you turn one on
  and get empty results, this is why — turn it back off.
- **Pagination is capped at 15 items per category or subcategory page,
  by design, not by accident.** DOTmed's own pagination beyond the
  first page uses `?offset=` query parameters, and DOTmed's robots.txt
  explicitly disallows `/browse/*?*` for every crawler. The house does
  not fetch what robots.txt disallows, whatever depth of data that
  leaves on the table (Rule 1). Coverage instead comes from crawling
  many category/subcategory entry points — each yields its own first 15
  — rather than paging deep into one. A single category input therefore
  reliably returns at most 15 stub-level items unless you list more
  categories.
- **`specifications`, `sellerLocation`, `sellerType`, `auctionEndsAt`,
  and `listedAt` are always null in this version.** DOTmed does not
  expose these consistently enough on the page (structured specs
  aren't reliably present; seller location is unstructured shipping
  prose; the auction countdown is rendered client-side in JavaScript,
  not in the static HTML this Actor fetches) to fill them without
  guessing. They're declared in the schema as reserved fields rather
  than omitted, so a future version can populate them without breaking
  the shape buyers already depend on.
- **Keyword search is not implemented.** DOTmed's site-wide search
  action points at `/listings/search/equipment.html`, a path this
  version has not verified is safe and complete to crawl (distinct from
  the `/listings/search_equipment.html` path robots.txt explicitly
  disallows). Category and manufacturer filtering are the supported way
  to narrow results for now.
- **The description field is a filter, not a guarantee.** DOTmed's
  standard shipping/returns/warranty/FDA-disclaimer boilerplate — which
  sometimes carries a private seller's own address in a template line —
  is cut from the description before output, and any remaining
  phone/email/contact-prose pattern is redacted. This catches the
  common shapes; it is not a certified absence of every possible
  personal-data pattern in free text a third party wrote.
- **How often the source changes:** DOTmed lists roughly 800,000+ active
  listings across all categories; individual auctions close on their
  own schedule (hours to weeks) and fixed listings can be posted, sold,
  or removed at any time. Run daily for freshness on a watched category.

### Legal

This Actor fetches only logged-out, publicly browseable pages —
category listings and individual item pages — that DOTmed's robots.txt
permits for a declared crawler. It never logs in, holds an account, or
solves a challenge, so DOTmed's account-level terms are not a contract
this house has entered.

DOTmed's Legal Notice and Listings Notice contain standard
anti-extraction language, including: "you are not permitted to copy,
transmit, publish, reproduce, redistribute, modify, sell, create
derivative works or publicly display" content "without prior written
authorization" (Legal Notice §2.5), and "You are not permitted to
extract any Content, including, data from this site for posting on
another website or publication" (Legal Notice §4). This house does not
hold a DOTmed account and has not agreed to those terms; the statement
DOTmed addresses to automated visitors is its robots.txt, and that is
the consent this house honors exactly — every path this Actor fetches
is one robots.txt permits. A buyer of this data who separately holds a
DOTmed account is bound by whatever terms they personally accepted;
that is between the buyer and DOTmed.

No personal data. Every field is either a business entity (a company
name in `sellerCompany`) or an inherent property of the equipment
listing itself (price, condition, manufacturer, category). Contact
person names, phone numbers, and email addresses are excluded at the
schema level — this Actor never emits a `sellerName`, `contactName`,
`phone`, or `email` field — and free-text fields are additionally
filtered for contact-prose patterns before output (see "Limits and
honesty" above). You are responsible for how you use this data
downstream.

### Changelog

See `CHANGELOG.md`.

### Support

Open an issue on this Actor's Issues tab. The house replies within one
day, in plain terms: what happened, what was done, when.

# Changelog

This Actor's version history is a separate document: https://apify.com/subrosa/dotmed-equipment/changelog.md

# Actor input Schema

## `categories` (type: `array`):

DOTmed top-level category slugs to crawl. Confirmed valid: "imaging", "cardiology", "dental", "surgical", "laboratory", "endoscopy", "respiratory", "neonatal", "homecare-rehab", "veterinary", "ophthalmology" — verified against the live site 2026-09-26; an invalid slug returns 0 items rather than an error. Each category page yields up to 15 listings without visiting a detail page, or up to `maxItems` when `scrapeDetails` is on.

## `manufacturers` (type: `array`):

Optional. Keep only items whose manufacturer name contains one of these (case-insensitive), e.g. "Siemens", "GE", "Philips". Leave empty to keep all manufacturers. This filters after extraction — it does not reduce the number of pages crawled.

## `maxItems` (type: `integer`):

Stop once this many items have been delivered. The default run must produce at least 10 items in under 5 minutes, so keep this low for quick testing and raise it for a full run.

## `scrapeDetails` (type: `boolean`):

When on, visits each listing's own page for price, condition, manufacturer, model, seller, and photos. When off, only the fields visible on the category page are returned (title, url, listing type, one photo) — faster and cheaper, but thin.

## `includeAuctions` (type: `boolean`):

Include items currently up for auction (priced by current bid, not an asking price).

## `includeListings` (type: `boolean`):

Include items with a fixed asking price (not up for auction).

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

Apify Proxy configuration. Defaults to no proxy (direct connection) — found empirically that DOTmed returns HTTP 200 but silently omits the embedded product data when the request comes from Apify's shared datacenter proxy pool, while a direct connection under the same declared identity gets full data. This is a data-availability finding, not a block worth fighting: Rule 1 says a source's own behavior is followed, and direct is simply what gets the real page. The house does not use residential proxies, default or otherwise.

## Actor input object example

```json
{
  "categories": [
    "imaging",
    "cardiology",
    "dental",
    "surgical",
    "laboratory",
    "endoscopy",
    "respiratory",
    "neonatal",
    "homecare-rehab",
    "veterinary",
    "ophthalmology"
  ],
  "manufacturers": [],
  "maxItems": 30,
  "scrapeDetails": true,
  "includeAuctions": true,
  "includeListings": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (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 = {
    "categories": [
        "imaging",
        "cardiology",
        "dental",
        "surgical",
        "laboratory",
        "endoscopy",
        "respiratory",
        "neonatal",
        "homecare-rehab",
        "veterinary",
        "ophthalmology"
    ],
    "manufacturers": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("subrosa/dotmed-equipment").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 = {
    "categories": [
        "imaging",
        "cardiology",
        "dental",
        "surgical",
        "laboratory",
        "endoscopy",
        "respiratory",
        "neonatal",
        "homecare-rehab",
        "veterinary",
        "ophthalmology",
    ],
    "manufacturers": [],
}

# Run the Actor and wait for it to finish
run = client.actor("subrosa/dotmed-equipment").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 '{
  "categories": [
    "imaging",
    "cardiology",
    "dental",
    "surgical",
    "laboratory",
    "endoscopy",
    "respiratory",
    "neonatal",
    "homecare-rehab",
    "veterinary",
    "ophthalmology"
  ],
  "manufacturers": []
}' |
apify call subrosa/dotmed-equipment --silent --output-dataset

```

## MCP server setup

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

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/yaa3By7WWoI3gQHs7/builds/HflPT5U56loOgB2e0/openapi.json
