# Minnesota DNR Timber Sales (`mdcurt/mn-dnr-timber-sales`) Actor

Minnesota state timber auction calendar (date, type, status, location, notice/results link) plus the DNR archive of sealed-bid tabulation records and notice-of-sale PDFs, as clean structured records.

- **URL**: https://apify.com/mdcurt/mn-dnr-timber-sales.md
- **Developed by:** [Matthew Curtis](https://apify.com/mdcurt) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 timber sale records

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

## Minnesota DNR Timber Sales

Structured feed of **Minnesota state timber auctions** from the Minnesota Department of Natural Resources (DNR) Division of Forestry. One run gives you every auction on the public [Timber Sales Calendar](https://webapps.dnr.state.mn.us/timber_sales_calendar/) and, optionally, the [Timber Auction Archive](https://www.dnr.state.mn.us/forestry/timbersales/auction-archive.html): the sealed-bid auction tabulation records (which tracts sold, to whom, at what price) and three years of archived notice-of-sale PDFs.

The DNR sells roughly 800,000 cords of timber a year through about 70 area and regional auctions. Loggers, mills, timber brokers, appraisers and forestry consultants track the calendar to know when notices are posted (about 30 days before each sale), when tracts become visible, and when results are published. This Actor turns the web pages into clean records so you can diff them, feed a spreadsheet, or trigger alerts when a notice goes *Final* or a sale goes *Completed*.

### What it collects

**Calendar records** (`recordType: "calendar"`), one per scheduled auction:

| Field | Example | Notes |
| --- | --- | --- |
| `id` | `calendar:2026-12-01:tower-area-regular` | Stable across runs |
| `saleDate` | `2026-12-01` | ISO date |
| `dayOfWeek` | `Tue` | |
| `auctionType` | `Regular` / `Intermediate` | Intermediate = reserved for purchasers with 30 or fewer employees |
| `status` | `In Development` / `Final` / `Completed` | DNR notice-of-sale status |
| `saleName` | `Tower Area Regular` | As printed |
| `area` | `Tower Area` | Sale name without the type words |
| `saleMethod` | `Oral` / `Sealed Bid` | |
| `time` | `9:00 AM` | |
| `location` | `Cook Community Center` | Or the bid-award note for sealed bids |
| `detailUrl` | link or `null` | Notice-of-sale / results page, present once the status is Final or Completed |
| `sourceUrl`, `scrapedAt` | | |

**Document records** (`recordType: "document"`), one per archive PDF, when *Include archive documents* is on:

| Field | Example |
| --- | --- |
| `id` | `document:notice-of-sale:20260910-little-falls-regular` |
| `documentType` | `Sealed Bid Tabulation Record` or `Notice of Sale` |
| `title` | `09/10/2026 Little Falls Regular` |
| `documentDate` | `2026-09-10` |
| `year` | `2026` |
| `saleName` | `Little Falls Regular` |
| `auctionType` | `Regular`, `Intermediate`, `Special` (decorative materials, salvage, Camp Ripley) or `All` (one document covering both Regular and Intermediate tracts) |
| `saleMethod` | `Oral` / `Sealed Bid` |
| `url` | absolute PDF link on `files.dnr.state.mn.us` or `webapps.dnr.state.mn.us` |
| `fileName` | `20260910 Little Falls Regular.pdf` |

A `SUMMARY` record in the key-value store reports rows found and kept, the filters applied, failed requests, and links to the DNR *Qualified Purchasers List* and *Notice of Unsold Timber Sales* PDFs (updated nightly by DNR).

### Input

| Input | Default | What it does |
| --- | --- | --- |
| `includeCalendar` | `true` | Collect the auction calendar (currently about 70 auctions, roughly 2 months ahead plus the last 30 days). |
| `includeDocuments` | `false` | Also collect the Timber Auction Archive documents. |
| `documentTypes` | both | `Sealed Bid Tabulation Record` and/or `Notice of Sale`. |
| `documentYears` | `2` | Keep only documents from the most recent N calendar years; `0` = whole archive (about 850 documents back to 2016). |
| `dateFrom` / `dateTo` | empty | ISO day bounds applied to `saleDate` and `documentDate`. |
| `auctionTypes` | all | Any of `Regular`, `Intermediate`, `Special`. Combined (`All`) documents always pass this filter. |
| `statuses` | all | Calendar statuses to keep, e.g. `["Final", "Completed"]` to get only auctions with a posted notice or results. |
| `maxItems` | `200` | Hard cap on records (calendar first, then documents). |

### Sample output record

```json
{
  "recordType": "calendar",
  "id": "calendar:2027-01-05:gna-sealed-bid-regular",
  "saleDate": "2027-01-05",
  "dayOfWeek": "Tue",
  "auctionType": "Regular",
  "status": "In Development",
  "saleName": "GNA Sealed Bid Regular",
  "area": "GNA",
  "saleMethod": "Sealed Bid",
  "time": "4:30 PM",
  "location": "Bid Awarding at the Two Harbors Area Office at 2:00 PM on January 6th",
  "detailUrl": null,
  "sourceUrl": "https://webapps.dnr.state.mn.us/timber_sales_calendar/",
  "scrapedAt": "2026-10-24T18:50:06.105Z"
}
```

### Pricing

Pay per event, using Apify's built-in events:

- **Actor start** – $0.02 per run (one event per GB of memory, minimum one). The Actor needs only 256 MB.
- **Timber sale record** – $0.005 per record in the dataset (calendar row or archive document).

A default run (calendar only, about 70 records) costs about $0.37 plus Apify platform usage. A full archive pull (`includeDocuments: true`, `documentYears: 0`, `maxItems: 1000`) is about $4.60. There is no charge for records filtered out by your input.

### Limitations

- DNR keeps auction results on the calendar for 30 days after each sale; older oral-auction results are not available online. Sealed-bid results stay in the archive as PDFs.
- Per-tract details (species, volume, appraised and bid prices, purchaser) live inside the linked notice-of-sale and tabulation PDFs. This Actor gives you the links and metadata; it does not parse the PDFs.
- The calendar shows *about two months* of upcoming auctions, so between seasons it can be short. Nothing is fabricated when the page is empty: a missing table fails the run with a clear message so you notice layout changes.
- Public data only. No login, no CAPTCHA, no personal data: purchaser names appear only inside DNR's own PDFs, which are linked, not scraped.

### Get alerted when records change

Pair this Actor with [Dataset Diff & Alert Monitor](https://apify.com/mdcurt/dataset-diff-monitor) to receive only the new, changed or removed records in Slack, Discord, Telegram or a webhook. Schedule the monitor, not this Actor: it runs this Actor for you, compares runs by `id`, and a run that finds nothing costs only the comparison. Status changes (a notice posted, results final) arrive as `changed` rows. The sister Actor for Washington is [wa-dnr-timber-sales](https://apify.com/mdcurt/wa-dnr-timber-sales).

```json
{
  "sourceActorId": "mdcurt/mn-dnr-timber-sales",
  "sourceInput": {
    "includeCalendar": true,
    "includeDocuments": false
  },
  "keyFields": [
    "id"
  ],
  "notifyOn": [
    "added",
    "changed"
  ],
  "labelField": "saleName",
  "urlField": "detailUrl",
  "slackWebhookUrl": "https://hooks.slack.com/services/…"
}
```

### Changelog

- **1.0.0** (2026-10-24) – Initial release: calendar + archive index, filters, SUMMARY record, stable ids.

# Actor input Schema

## `includeCalendar` (type: `boolean`):

Collect every upcoming and recently completed state timber auction from the DNR Timber Sales Calendar (date, auction type, notice status, sale name, time, location, link to the notice of sale or results when posted). One record per auction.

## `includeDocuments` (type: `boolean`):

Also collect the Timber Auction Archive: links to sealed-bid auction tabulation records (who bought which tracts and at what price) and archived notice-of-sale PDFs, with the auction date, sale name and auction type parsed from each title. One record per document.

## `documentTypes` (type: `array`):

Which archive documents to include when 'Include archive documents' is on.

## `documentYears` (type: `integer`):

Keep only archive documents from the most recent N calendar years (counting the current year). 0 keeps the whole archive (about 800 documents going back to 2016).

## `dateFrom` (type: `string`):

Keep only calendar rows and documents dated on or after this day. Leave empty for no lower bound.

## `dateTo` (type: `string`):

Keep only calendar rows and documents dated on or before this day. Leave empty for no upper bound.

## `auctionTypes` (type: `array`):

Keep only these auction types. Regular auctions are open to all qualified purchasers; Intermediate auctions are reserved for purchasers with 30 or fewer employees; Special covers decorative-materials, salvage and Camp Ripley sales. Empty = all.

## `statuses` (type: `array`):

Keep only calendar rows with these notice-of-sale statuses, e.g. Final (notice posted, tracts visible) or Completed (results posted). Empty = all statuses.

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

Stop after this many records in total (calendar rows first, then documents). Each record is one paid dataset item.

## Actor input object example

```json
{
  "includeCalendar": true,
  "includeDocuments": false,
  "documentTypes": [
    "Sealed Bid Tabulation Record",
    "Notice of Sale"
  ],
  "documentYears": 2,
  "auctionTypes": [],
  "statuses": [],
  "maxItems": 200
}
```

# Actor output Schema

## `records` (type: `string`):

One record per auction (recordType = calendar) or archive document (recordType = document) with stable ids, ISO dates and absolute links.

## `summary` (type: `string`):

SUMMARY record: counts of rows found and kept, filters applied, failed requests, and links to the DNR qualified-purchasers list and notice of unsold timber sales.

# 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 = {
    "documentTypes": [
        "Sealed Bid Tabulation Record",
        "Notice of Sale"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mdcurt/mn-dnr-timber-sales").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 = { "documentTypes": [
        "Sealed Bid Tabulation Record",
        "Notice of Sale",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mdcurt/mn-dnr-timber-sales").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 '{
  "documentTypes": [
    "Sealed Bid Tabulation Record",
    "Notice of Sale"
  ]
}' |
apify call mdcurt/mn-dnr-timber-sales --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mdcurt/mn-dnr-timber-sales"
        }
    }
}
```

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/SfEOaJBTJSANP8TRE/builds/wpB6AnNpmaVg20yII/openapi.json
