# PlanetBids Scraper – Government Bids & RFPs (`gallant_illustration/planet-bids-scrapper`) Actor

PlanetBids bid tracker: open bids, RFPs and RFQs from 272 city, county, school and water district portals (mostly California). Due dates, categories, estimated value, documents, addenda, awards and bid results. No login.

- **URL**: https://apify.com/gallant_illustration/planet-bids-scrapper.md
- **Developed by:** [Alexei Pannicov](https://apify.com/gallant_illustration) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 bids

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

## PlanetBids Scraper – Government Bids & RFPs

PlanetBids scraper and bid tracker: open government bids, RFPs and RFQs from city, county, school district and water district portals on [PlanetBids](https://www.planetbids.com/) (mostly California), plus bid results and awarded contracts (who won and for how much). Collect every public bid opportunity (bids, RFPs, RFQs, RFIs, qualifications) with due dates, categories, estimated value, documents and addenda, no login needed. PlanetBids hosts procurement portals for hundreds of US public agencies: cities, counties, school districts, water and transit districts. This Actor reads each portal's own JSON API over plain HTTP (no browser), so runs are fast and cheap, and every field comes typed and normalised.

### What you get

- **Every bid on a portal**, with stage (planning, bidding, closed, award pending, awarded, canceled, rejected), type, invitation number, issue and due dates with the agency's time zone offset, and categories with names.
- **Bid details** (optional): scope and details HTML, estimated value (text and parsed USD), location, department, license requirement, duration, liquidated damages, pre-bid meeting, Q\&A cutoff, award date and notes, special notices.
- **Extras** (optional, one request each): document metadata, addenda with attachments, line items, pre-bid meeting, **awards (winning vendor and amount)**, bid results (all bidders and amounts), prospective bidders.
- **A bundled catalog of 272 PlanetBids portals in 12 states (250 in California)**, so you can scrape every catalog portal in a state without looking up IDs.
- **Run records** that tell you whether the data is complete: `RUN_SUMMARY` (per-portal counts, failures, WAF challenges) and `CONTRACT_DRIFT` (any change in the PlanetBids API seen during the run).

### Use cases

- **Find new bids to win:** track open bids and RFPs by keyword, category code or bid type across the agencies you sell to, and run it on a schedule.
- **Competitive intelligence:** with `awards` and `bidResults`, see who won each contract, for how much, and what every other bidder offered.
- **Sales and lead generation:** list the public agencies that buy what you sell, with departments, locations and estimated contract values.

### How to use it

1. Pick agencies by name from the catalog, or pick whole states. For a portal that is not in the catalog, enter its ID such as `14319` or a portal URL such as `https://vendors.planetbids.com/portal/14319/bo/bo-search`.
2. Choose filters: stages, bid types, keyword, category codes, due date range, posted-after date.
3. Turn on **Include bid details** and pick extras if you need more than the list data.
4. Set **Max items** (default 100, 0 = unlimited) and run.

The portal ID is the number after `/portal/` in any PlanetBids vendor URL.

### Input

| Field                      | Type                                                                                               | Default       | Notes                                                                                                               |
| -------------------------- | -------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `portals`                  | list of IDs or URLs                                                                                | `[]`          | Portals not in the catalog. Combined with the catalog selections.                                                   |
| `catalogPortals`           | list of catalog portal IDs                                                                         | `[]`          | Shown as agency names in the input form.                                                                            |
| `catalogStates`            | list of US states                                                                                  | `[]`          | Adds every catalog portal in these states.                                                                          |
| `allCatalogPortals`        | boolean                                                                                            | `false`       | Adds every catalog portal.                                                                                          |
| `stages`                   | `planning`, `bidding`, `closed`, `awardPending`, `awarded`, `canceled`, `rejected`                 | `["bidding"]` | Empty = all. One stage is filtered by the API; several are filtered by the Actor after listing everything (slower). |
| `bidTypes`                 | `bid`, `rfi`, `rfp`, `rfq`, `rfqual`, `ipwb`                                                       | `[]`          | Empty = all. Same one-vs-several rule as stages.                                                                    |
| `keyword`                  | text                                                                                               | `""`          | Full-text search by the API.                                                                                        |
| `categoryCodes`            | list                                                                                               | `[]`          | Category (NIGP-like) codes, e.g. `91200`. Any match.                                                                |
| `dueDateFrom`, `dueDateTo` | `YYYY-MM-DD`                                                                                       | none          | Sent to the API.                                                                                                    |
| `postedAfter`              | `YYYY-MM-DD`                                                                                       | none          | Filtered by the Actor; with the default sort (issue date, newest first) paging stops early.                         |
| `sortBy`                   | `issueDate`, `title`, `invitationNumber`, `dueDate`, `stage`                                       | `issueDate`   | Matters with `maxItems`.                                                                                            |
| `sortOrder`                | `desc`, `asc`                                                                                      | `desc`        |                                                                                                                     |
| `includeDetails`           | boolean                                                                                            | `false`       | One extra request per bid.                                                                                          |
| `include`                  | `documents`, `addenda`, `lineItems`, `preBidMeeting`, `awards`, `bidResults`, `prospectiveBidders` | `[]`          | Needs `includeDetails`. One request per extra per bid.                                                              |
| `includeContacts`          | boolean                                                                                            | `false`       | Personal data, see below.                                                                                           |
| `maxItems`                 | integer                                                                                            | `100`         | Total across portals. `0` = unlimited.                                                                              |
| `maxRequestsPerMinute`     | integer                                                                                            | `50`          | Keep at 60 or less without a rotating proxy.                                                                        |
| `maxWafChallenges`         | integer                                                                                            | `5`           | Stop after this many WAF challenges in a row.                                                                       |
| `wafBackoffSecs`           | integer                                                                                            | `150`         | Pause after a WAF challenge (60-280).                                                                               |
| `proxyConfiguration`       | proxy                                                                                              | no proxy      | Not needed at the default rate.                                                                                     |

Example:

```json
{
    "portals": ["14319", "https://vendors.planetbids.com/portal/17950/bo/bo-search"],
    "stages": ["awarded"],
    "bidTypes": ["bid", "rfp"],
    "includeDetails": true,
    "include": ["awards"],
    "maxItems": 50
}
```

#### Extras are only fetched when the agency publishes them

Each bid tells whether its agency publishes awards, bid results and a pre-bid meeting. The Actor requests an extra only when it is published. Otherwise `extras.<name>` is `notAvailable` and the field is `null`. Award amounts are `null` when the agency publishes the winner but not the amount. Statuses: `ok`, `notAvailable`, `failed` (the request or parsing failed, the item is still saved), `skipped`.

### Output

One dataset item per bid. List fields are always present. Detail fields appear when `includeDetails` is on and `detailStatus` is `ok`. A real item from Kern High School District (portal 14319), shortened:

```json
{
    "portalId": 14319,
    "portalName": "Kern High School District",
    "portalState": "California",
    "portalTimeZone": "America/Los_Angeles",
    "bidId": 146762,
    "url": "https://vendors.planetbids.com/portal/14319/bo/bo-detail/146762",
    "title": "Vista Continuation High School Modernization: Trash Haul-Off",
    "invitationNumber": "Bid No. 6948",
    "stage": "bidding",
    "stageCode": 3,
    "bidType": "bid",
    "bidTypeCode": 1,
    "responseFormat": "paper",
    "responseFormatCode": 2,
    "issuedAt": "2026-09-30T10:19:57-07:00",
    "dueAt": "2026-10-21T14:00:00-07:00",
    "categories": [
        { "code": "11111", "name": "Builder's Exchange & Plan Rooms" },
        { "code": "91027", "name": "Garbage/Trash Removal, Disposal And/Or Treatment" }
    ],
    "detailStatus": "ok",
    "scrapedAt": "2026-10-08T14:15:02.252Z",
    "scope": "<p>Provide dumpsters and dumpster haul-off. Dumpsters provided are for the use of all Prime Contractors on site.</p>",
    "estimatedValueText": "$98,000.00",
    "estimatedValueUsd": 98000,
    "licenseRequirement": "B or C-21 or C-61 and D-63",
    "address": {
        "line1": "5801 Sundale Avenue",
        "line2": null,
        "city": "Bakersfield",
        "county": "Kern",
        "zip": "93309",
        "state": "California"
    },
    "department": "Business Services Department",
    "duration": "490 Calendar Days",
    "liquidatedDamages": "$2,000.00 per calendar day",
    "preBidMeetingAt": "2026-10-07T10:00:00-07:00",
    "preBidMeetingMandatory": false,
    "extras": { "documents": "ok", "awards": "notAvailable" },
    "documents": [
        {
            "documentId": 646708,
            "title": "Specifications",
            "filename": "Specifications.pdf",
            "sizeBytes": 6589621,
            "uploadedAt": "2026-09-30T08:06:35-07:00",
            "isRecalled": false,
            "publiclyVisible": false,
            "downloadUrl": "https://files-prod01.planetbids.com/KHSD/BMfiles/20260930080635007%20Specifications.pdf"
        }
    ],
    "awards": null
}
```

Notes on the data:

- **Dates** are ISO 8601 with the agency's UTC offset (PlanetBids stores local time without a zone). Empty dates are `null`.
- **Codes:** `stage`, `bidType` and `responseFormat` are enums. If PlanetBids adds a new code, the value is `unknown`, the raw number stays in `stageCode` / `bidTypeCode` / `responseFormatCode`, the item is still saved, and the code is listed in `CONTRACT_DRIFT`.
- **Stage** comes from the bid list. The detail endpoint reports closed bids as stage 3 "Closed", so the list value is the reliable one.
- **Estimated value** is free text at PlanetBids (`"$150,000.00"`, `"N/A"`). `estimatedValueUsd` is set only when the text is a plain dollar amount.
- **Documents** are metadata only. The file host is behind a CAPTCHA, so `downloadUrl` does not download with plain HTTP.
- **HTML fields** (`scope`, `detailsHtml`, `notesHtml`) are returned as published. Escape or sanitise them before rendering.

#### Run records (key-value store)

- `RUN_SUMMARY`: end reason (`completed`, `maxItemsReached`, `chargeLimitReached`, `wafBlocked`, `aborted`), items pushed, failed details, skipped records, failed requests, WAF challenges, failed extras, and per portal: status (`ok`, `notFound`, `failed`, or `skipped` when the run stopped before the portal finished listing, e.g. at Max items), name, `totalBids` reported by PlanetBids, listed and pushed counts.
- `CONTRACT_DRIFT`: new keys, missing keys, type changes and unknown codes seen in API responses. Empty means the API matched the expected contract.

The run's status message sums it up, for example `Done: 1475 bids, 0 details failed, 0 portals not ok, 0 contract drift warnings`. When the run collects nothing (no bids pushed and every portal unknown or failing), it ends as failed with a `Failed: …` message. A run aborted gracefully (the `aborting` event, API `abort?gracefully=true`) saves both records and reports `Stopped (aborted): …`. An immediate abort, the platform default, kills the run at once, so neither the records nor the status message are written.

### Personal data

By default the Actor outputs no personal data. With `includeContacts` on, it adds the bid contact (name, phone, email), bid result respondent names and phones, and the prospective bidders list (contact names, phones, masked emails). `prospectiveBidders` is ignored without `includeContacts`. Only enable it if you have a legitimate reason and handle the data according to the laws that apply to you.

### Pricing

Pay per event, platform usage included:

- `bid-list-item`: **$0.003 per bid** ($3 per 1,000) with list data. Also charged when a bid's detail request failed and its list data is saved instead.
- `bid-detail-item`: **$0.008 per bid** ($8 per 1,000) with details and the requested extras.

Each bid is charged once, by one of the two events; extras (documents, addenda, awards…) are not charged separately. Example: 1,000 bids with details cost about $8. Paid Apify plans get lower per-event prices.

### Rate limits and reliability

PlanetBids protects its API with an AWS WAF rate rule per IP. About 1 request per second runs clean; about 5 per second gets the IP challenged within half a minute, and the block lifts 1-2 minutes after traffic stops. The Actor:

- spaces every request (including extras) by `60 / maxRequestsPerMinute` seconds and runs one request at a time;
- treats a WAF challenge (HTTP 202) as a soft block: pauses `wafBackoffSecs`, rotates the session, retries the same request, and stops cleanly after `maxWafChallenges` challenges in a row, keeping everything pushed so far (`endReason: wafBlocked`);
- retries other failures 3 times; a bid whose detail keeps failing is saved with list data, `detailStatus: failed` and the error;
- never stops on bad data: a record it cannot identify is skipped and counted, any other field degrades to `null` and is reported in `CONTRACT_DRIFT`.

Throughput at the default 50 requests per minute: a portal with 1,500 bids lists in about 1 minute; with details, about 30 minutes; each extra adds about the same again. For large jobs, raise `maxRequestsPerMinute` only together with a rotating proxy.

### 🔗 Related procurement Actors

- [**Bonfire / Euna government bids**](https://apify.com/gallant_illustration/bonfire-procurement-scraper) — open RFPs and bids from hundreds of US and Canadian agencies on Bonfire / Euna Procurement. With PlanetBids, it covers two of the largest municipal bid platforms.
- [**Sourcewell awarded contracts & vendors**](https://apify.com/gallant_illustration/sourcewell-scrapper) — the Sourcewell cooperative contract registry with awarded suppliers, categories and an expiring-contracts filter.
- [**OMNIA Partners / NCPA cooperative contracts**](https://apify.com/gallant_illustration/omnia-partners-scrapper) — OMNIA Partners, NCPA and US Communities contracts with lead agencies, expiration dates and contract PDFs.

**From expiring contract to new bid:** when a cooperative contract is about to expire, agencies that bought through it often run their own solicitation. Find contracts ending soon with `expiresWithinDays` in the Sourcewell or OMNIA Actor, then catch the new bid here or on Bonfire with a keyword filter.

### Limitations

- Documents are listed, not downloaded (CAPTCHA on the file host).
- The portal catalog holds the portals found in public web crawls, then verified against the PlanetBids API; it misses portals that were never crawled. Any portal works when given by ID or URL.
- Some codes have no published labels (award type, bond formats, bid template type), so they are not output.
- The API is undocumented and can change without notice; `CONTRACT_DRIFT` and a weekly API contract check exist for that reason.

# Changelog

This Actor's version history is a separate document: https://apify.com/gallant_illustration/planet-bids-scrapper/changelog.md

# Actor input Schema

## `portals` (type: `array`):

Portals that are not in the catalog list below: PlanetBids portal IDs (the number after <code>/portal/</code> in the URL, e.g. <code>14319</code>) or any portal URL such as <code>https://vendors.planetbids.com/portal/14319/bo/bo-search</code>. Combined with the catalog selections below.

## `catalogPortals` (type: `array`):

Pick agencies from the bundled PlanetBids portal catalog by name. Only portals with the bid module enabled are offered.

## `catalogStates` (type: `array`):

Add every portal from the bundled PlanetBids portal catalog located in these states.

## `allCatalogPortals` (type: `boolean`):

Add every portal from the bundled catalog. A full run takes long at the safe request rate (about 1 request per second).

## `stages` (type: `array`):

Bid stages to return. Empty = all stages. One stage is filtered by the API; several are filtered by the Actor after listing all bids (slower).

## `bidTypes` (type: `array`):

Solicitation types. Empty = all. One type is filtered by the API; several are filtered by the Actor.

## `keyword` (type: `string`):

Full-text search sent to the PlanetBids API.

## `categoryCodes` (type: `array`):

Category (NIGP-like) codes, e.g. <code>91200</code>. A bid matches if it has any of them.

## `dueDateFrom` (type: `string`):

Only bids due on or after this date (YYYY-MM-DD). Sent to the API.

## `dueDateTo` (type: `string`):

Only bids due on or before this date (YYYY-MM-DD). Sent to the API.

## `postedAfter` (type: `string`):

Only bids issued on or after this date (YYYY-MM-DD, portal local time). Filtered by the Actor; with sort by issue date, newest first, paging stops early.

## `sortBy` (type: `string`):

Order in which the API lists bids. Matters with Max items.

## `sortOrder` (type: `string`):

Sort direction.

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

Fetch the bid detail page data (scope, estimated value, location, department, pre-bid meeting, notices). One extra request per bid; charged as a detail item.

## `include` (type: `array`):

Extra data per bid, one request each. Needs Include bid details. Awards and bid results are fetched only when the agency publishes them.

## `includeContacts` (type: `boolean`):

Output personal data: bid contact name, phone and email, bid result respondent names and phones, and the prospective bidders list. Off by default.

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

Stop after this many dataset items in total. 0 = unlimited.

## `maxRequestsPerMinute` (type: `integer`):

PlanetBids blocks an IP (AWS WAF challenge) at roughly 5 requests per second. Keep at 60 or less unless a proxy rotates IPs.

## `maxWafChallenges` (type: `integer`):

Stop the run after this many WAF challenges in a row. Items pushed so far are kept.

## `wafBackoffSecs` (type: `integer`):

Pause after a WAF challenge before retrying. The block lifts about 1-2 minutes after traffic stops.

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

Optional proxy. Not needed at the default request rate; Apify Proxy is a paid feature.

## Actor input object example

```json
{
  "portals": [],
  "catalogPortals": [
    "14319"
  ],
  "catalogStates": [],
  "allCatalogPortals": false,
  "stages": [
    "bidding"
  ],
  "bidTypes": [],
  "keyword": "",
  "categoryCodes": [],
  "sortBy": "issueDate",
  "sortOrder": "desc",
  "includeDetails": false,
  "include": [],
  "includeContacts": false,
  "maxItems": 100,
  "maxRequestsPerMinute": 50,
  "maxWafChallenges": 5,
  "wafBackoffSecs": 150,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

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

No description

## `contractDrift` (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 = {
    "catalogPortals": [
        "14319"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gallant_illustration/planet-bids-scrapper").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 = { "catalogPortals": ["14319"] }

# Run the Actor and wait for it to finish
run = client.actor("gallant_illustration/planet-bids-scrapper").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 '{
  "catalogPortals": [
    "14319"
  ]
}' |
apify call gallant_illustration/planet-bids-scrapper --silent --output-dataset

```

## MCP server setup

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

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/J8dd9I8mciuxLqKt5/builds/KL9kxjNDCOgo8Vis8/openapi.json
