# WSDOT Bid Tabulations Scraper (`automation-lab/wsdot-construction-bid-tabulations`) Actor

Export official WSDOT construction bid tabulations by letting month/year or PDF URL. Get bidder item quantities, observed unit prices, totals and PDF/page provenance.

- **URL**: https://apify.com/automation-lab/wsdot-construction-bid-tabulations.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.44 / 1,000 item processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## WSDOT Bid Tabulations Scraper

Export **WSDOT bid tabulations** into bidder-level construction item records for Washington contractor estimating and historical bid-price benchmarking. Select a letting month/year or supply official WSDOT text-PDF URLs. Results include contract, bid-opening date, bidder identity, item description, quantity, unit, observed unit price, total amount, and PDF/page provenance.

### Who is it for?

- Estimators comparing observed unit prices for similar highway work.
- Contractors reviewing bidder item prices across past lettings.
- Analysts preparing Washington transportation construction cost datasets.

These are observed bids, not recommended prices, inflation-adjusted estimates, or guarantees of future costs.

### Why use it?

Bid-check reports put engineer estimates, bidder prices, totals, and percentage differences beside each other. This Actor reconstructs the text coordinates and normalizes page rotation instead of flattening all numbers into one list. It preserves bidder identity and distinguishes a unit price from a lump-sum total.

The workflow is standalone: it exports historical item-price observations, not procurement opportunity notices.

### Getting started

1. Open the input form.
2. Enter the bid-opening year and month.
3. Set the maximum bidder-item rows and maximum PDF documents.
4. Run the Actor.
5. Export the default dataset as JSON, CSV, Excel, or another Apify-supported format.
6. Check `PARSING_REPORT` before treating a run as complete coverage.

Example monthly input:

```json
{"year":2025,"month":1,"maxItems":20,"maxDocuments":5}
```

January 2025 currently contains the XE3584 text report. The number of rows depends on bidders and bid items, not on a promised fixed dataset size.

### Inputs

| Field | Meaning |
|---|---|
| `year` | Bid-opening year, 2000–2100; required for monthly discovery |
| `month` | Bid-opening month, 1–12; required for monthly discovery |
| `pdfUrls` | Optional list of official WSDOT HTTPS text-PDF URLs; replaces index discovery |
| `maxItems` | Maximum bidder-item records, default 20, maximum 100,000 |
| `maxDocuments` | Maximum sequential PDF downloads, default 5, maximum 50 |

Supply year and month together for index discovery, or at least one PDF URL. With explicit PDFs, either optional date filter still applies to the actual bid-opening date in the document.

Explicit-PDF example:

```json
{
  "pdfUrls":["https://wsdot.wa.gov/sites/default/files/2026-01/Contracts-bidtabulation-26B002.pdf"],
  "maxItems":20,
  "maxDocuments":1
}
```

Only `https://wsdot.wa.gov/sites/default/files/` PDF URLs are accepted. Other hosts, authenticated URLs, and URL query strings are rejected.

### Output fields

One bidder's observation for one bid item is one dataset row. A contract with three bidders and ten items may produce thirty rows.

| Field | Description |
|---|---|
| `contractNumber` | Contract identifier as printed, preserving leading zeroes |
| `jobNumber` | PS\&E job number, when available |
| `lettingDate` | Bid-opening date in YYYY-MM-DD format |
| `bidderName` | Bidder name as printed, including source truncation |
| `bidderId` | Contractor number, when available |
| `itemNumber` | Printed item number |
| `itemDescription` | Printed item description |
| `unit` | Source unit such as L.F., EACH, TON, or L.S. |
| `quantity` | Observed estimated quantity, or null |
| `unitPrice` | Observed bidder unit price in USD, or null |
| `totalAmount` | Printed bidder item total in USD |
| `currency` | USD |
| `parsingStatus` | `parsed` or `total_only` |
| `sourceUrl` | Official source PDF |
| `sourcePage` | One-based physical PDF page |

Engineer estimates and percentage-difference columns are not exported as bidder prices. Contract execution and report-generation dates are not used as letting dates.

### Example record

This is a representative observed item from the January 2025 XE3584 report:

```json
{
  "contractNumber":"XE3584",
  "jobNumber":"24A031",
  "lettingDate":"2025-01-29",
  "bidderName":"CECCANTI, INC.",
  "bidderId":"100420",
  "itemNumber":"3",
  "itemDescription":"REMOVING CONC. BARRIER",
  "unit":"L.F.",
  "quantity":2688,
  "unitPrice":11,
  "totalAmount":29568,
  "currency":"USD",
  "parsingStatus":"parsed",
  "sourceUrl":"https://wsdot.wa.gov/sites/default/files/2025-01/Contracts-bidtabulation-24A031.pdf",
  "sourcePage":1
}
```

### Parsing status and coverage

`parsed` means the parser observed both a quantity and a unit price. An observed zero unit price remains zero.

`total_only` means the bidder's printed item total is available but no usable unit-price observation is present, for example a lump-sum item. Quantity and unit price stay null; the Actor never divides totals to manufacture an observed price.

`PARSING_REPORT` records page statuses, accepted row counts, source URLs, and limit flags. Summary pages may contain no bid items. Unknown layouts and image-only pages are reported, not OCR-processed. A PDF with no supported rows fails the run rather than returning fabricated records.

A result limit may stop within a document or bidder group. A document limit may leave later contracts unvisited. Inspect the report and raise limits when necessary. Source links follow the official monthly index order; this is not a ranked or sorted search.

### How much does it cost to export WSDOT bid tabulations?

Pay-per-event pricing includes a one-time run-start event and a per-row event. Each delivered bidder/item observation is one result. Diagnostics have no separate result charge.

The one-time start price is **$0.025 per run**. Per bidder-item row prices use Apify's qualifying monthly Store-spend tiers:

| Tier | Price per row |
|---|---:|
| FREE | $0.00276 |
| BRONZE | $0.0024 |
| SILVER | $0.001872 |
| GOLD | $0.00144 |
| PLATINUM | $0.00144 |
| DIAMOND | $0.00144 |

At BRONZE, estimated Actor charges are $0.049 for 10 rows, $0.073 for 20 rows, and $0.265 for 100 rows, including the start event. A valid empty run still incurs the start event. The Store pricing panel and your account's applicable tier are authoritative. Set a run charge limit as well as input limits to control spending.

### Integrations

- Export CSV to a spreadsheet; group by item description and unit before comparing prices.
- Send the dataset to a database or warehouse and deduplicate using contract, bidder identity, and item number.
- Join records to your own project characteristics; unit price alone does not control for project conditions.
- Schedule a monthly input through Apify and use a webhook to deliver the dataset. Scheduling reruns the extraction; the Actor does not maintain cross-run change detection or alerts.

### API usage

Keep your Apify token in an environment variable, never in shared notebooks or committed source.

#### cURL

```bash
curl -X POST \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"year":2025,"month":1,"maxItems":100,"maxDocuments":5}' \
  'https://api.apify.com/v2/acts/automation-lab~wsdot-construction-bid-tabulations/run-sync-get-dataset-items'
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/wsdot-construction-bid-tabulations')
  .call({ year: 2025, month: 1, maxItems: 100, maxDocuments: 5 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/wsdot-construction-bid-tabulations').call(
    run_input={'year': 2025, 'month': 1, 'maxItems': 100, 'maxDocuments': 5})
print(client.dataset(run['defaultDatasetId']).list_items().items)
```

### MCP use

Add the Actor-specific Apify MCP endpoint to Claude Code:

```bash
claude mcp add --transport http apify \
  'https://mcp.apify.com?tools=automation-lab/wsdot-construction-bid-tabulations'
```

Claude Desktop, Cursor, and VS Code HTTP MCP configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url":"https://mcp.apify.com?tools=automation-lab/wsdot-construction-bid-tabulations"
    }
  }
}
```

Example prompt: “Export January 2025 WSDOT bid-item observations, limit to 100 rows, and summarize unit prices by item and unit. Flag total-only rows instead of interpreting them as unit prices.”

Configure Apify authentication in your MCP client. AI summaries should retain source links and not imply complete coverage when a limit was reached.

### Limitations

The Actor reads public WSDOT indexes and downloadable text bid-check PDFs. It does not access private accounts, solve challenges, process arbitrary uploaded PDFs, or provide scanned-PDF OCR.

Historical PDF layouts may vary. Tested examples include rotated 2023/2025 reports and unrotated 2026 reports; this is not a guarantee that every historical report is parseable. PDF upload-folder dates are not letting dates, especially for migrated archives.

Downloads are sequential with bounded transient retries and request timeouts. PDFs above 20 MB or 500 pages are rejected. Source outages, non-PDF responses, and unsupported all-empty documents fail visibly; earlier accepted rows may remain in a failed run's dataset.

### Legality and responsible use

Use public records responsibly, observe applicable source terms and laws, and confirm important pricing decisions against the source document. This Actor is independent and is not endorsed by WSDOT.

### Data handling and support

The runtime uses no AI or paid third-party data API. It downloads public source documents directly and holds PDF bytes transiently in memory. Only public bidder identities and bid-item records are saved; addresses are not exported. No credentials or source PDF text are logged.

Datasets, parsing reports and run logs remain in your Apify storage under your account's retention settings until you delete them. The Actor creates no external cross-run cache. Delete a run's dataset and key-value store through Apify Console or the API to remove stored results. Source documents remain published by WSDOT independently. Apify hosts execution and storage under its platform terms.

For extraction problems, open the Actor's Apify Issues tab with input, run link and the source/page affected. Avoid posting credentials or private information.

### Related workflows

This Actor is intentionally standalone: a procurement-notice scraper supplies opportunities, not bidder item-price history. Combine this dataset with your own project records rather than substituting unrelated notice records for unit-price observations.

### FAQ and troubleshooting

**Why is a unit price null?**
A lump-sum or estimated-total item may have no meaningful printed unit price. Use `totalAmount` and inspect `parsingStatus` rather than treating null as zero.

**Why are there multiple rows for an item?**
Each bidder is a separate observation. Later PDF pages may show additional bidder columns.

**Why did a month return no rows?**
The selected year/month may have no linked contracts, or supplied PDFs may not match optional date filters. Check `PARSING_REPORT`. Failed downloads and unsupported documents are errors, not valid empty source results.

**Why did only part of a contract appear?**
Raise `maxItems`; a row limit can stop within the PDF. Raise `maxDocuments` to include later linked contracts.

**Can I use it for every DOT?**
No. This Actor supports official Washington WSDOT monthly indexes and compatible WSDOT text-PDF layouts only.

**Does scheduling detect changed prices?**
No. Store results externally and compare runs yourself. The Actor deduplicates within a run, not across runs.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/wsdot-construction-bid-tabulations/changelog.md

# Actor input Schema

## `year` (type: `integer`):

Year of bid opening, not PDF upload or contract execution. Required with month when no PDF URLs are supplied.

## `month` (type: `integer`):

Bid-opening month from 1 (January) through 12 (December). Required with year for monthly discovery.

## `pdfUrls` (type: `array`):

Optional official HTTPS wsdot.wa.gov/sites/default/files/ text PDFs. Replaces monthly discovery; year and month still filter results when supplied.

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

Stop after this many bidder-item records. Each bidder's price for one item is one row.

## `maxDocuments` (type: `integer`):

Maximum sequential PDF downloads. Monthly results retain official link order.

## Actor input object example

```json
{
  "year": 2025,
  "month": 1,
  "maxItems": 20,
  "maxDocuments": 5
}
```

# Actor output Schema

## `dataset` (type: `string`):

Bidder item rows from this run.

## `report` (type: `string`):

Parsing report from this run.

# 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 = {
    "year": 2025,
    "month": 1,
    "maxItems": 20,
    "maxDocuments": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/wsdot-construction-bid-tabulations").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 = {
    "year": 2025,
    "month": 1,
    "maxItems": 20,
    "maxDocuments": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/wsdot-construction-bid-tabulations").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 '{
  "year": 2025,
  "month": 1,
  "maxItems": 20,
  "maxDocuments": 5
}' |
apify call automation-lab/wsdot-construction-bid-tabulations --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/wsdot-construction-bid-tabulations"
        }
    }
}
```

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/uLBkWgZkxtizjh44h/builds/dbIiWQlfFCIa1TW5V/openapi.json
