# NYC PASSPort Procurement Scraper (RFPs, Bids & Contracts) (`jserle/nyc-passport-procurement`) Actor

NYC PASSPort procurement data: open and historic RFPs, bids and M/WBE solicitations plus awarded NYC government contracts with vendor, amounts and dates, from the PASSPort Public portal. Filter by keyword, agency, method, category, status and date. $0.01/record.

- **URL**: https://apify.com/jserle/nyc-passport-procurement.md
- **Developed by:** [Jonathan Serle](https://apify.com/jserle) (community)
- **Categories:** Lead generation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 procurement 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## NYC PASSPort Procurement Scraper (RFPs, Bids & Contracts)

Pulls New York City government procurement data from the PASSPort Public portal into clean JSON records: open and historic solicitations (RFPs, competitive sealed bids, sole source, micropurchase and M/WBE set-asides) and awarded city contracts with vendor, award and current amounts, encumbered and paid totals, and start, end and registration dates. Filter by keyword, agency, procurement method, industry category, status and date.

PASSPort Public is a browser table: filterable on screen, awkward to pull into anything else. Two datasets, one schema:

- **Solicitations** - open and historic bidding opportunities across every mayoral agency. About 12,000 records.
- **Contracts** - awarded contracts with the vendor, amounts, dates, and the vendor's certification type and corporate structure. About 62,000 records.

### Who uses this data

- **Gov-contracting BD teams and NYC vendors** tracking new RFPs and bids by agency and industry, newest first, without watching the portal.
- **M/WBE firms** tracking set-asides. Filter `method` to `M/WBE` and you get the noncompetitive small-purchase pipeline for your industry.
- **Prime contractors and sales teams** researching who already holds a contract with a given agency, for how much, and until when.
- **Bid aggregators and govtech SaaS** that need NYC coverage without maintaining a scraper against a portal that ships its data as a JavaScript blob.
- **Researchers, journalists and watchdogs** studying award patterns, vendor concentration, or M/WBE participation over time.

### What you get

One dataset item per record, newest first. Both modes share the same core fields, so you can pipe them into one table.

```json
{
  "source": "passport-public",
  "mode": "solicitations",
  "id": "38092",
  "number": "85026B0100",
  "title": "85026B0100-PWQMAHVAC Maspeth Town Hall HVAC Upgrade",
  "agency": "DEPARTMENT OF DESIGN AND CONSTRUCTION",
  "method": "Competitive Sealed Bid",
  "category": "Construction",
  "subCategory": "HVAC Services",
  "status": "Planned",
  "releaseDate": "2025-11-21T00:00:00-05:00",
  "dueDate": "2026-08-27T14:00:00-04:00",
  "url": "https://passport.cityofnewyork.us/page.aspx/en/bpm/process_manage_extranet/38092",
  "program": "PUBLIC BUILDINGS",
  "bpmId": "36592",
  "scrapedAt": "2026-08-23T04:33:08.096Z"
}
```

Contract items add `vendor`, `contractId`, `contractType`, `awardAmount`, `currentContractAmount`, `totalEncumberedAmount`, `totalPaidAmount`, `startDate`, `endDate`, `registrationDate`, `certificationType`, `ethnicity` and `corporateStructure`. Amounts arrive as numbers, not `"$3,749.95"` strings. For contracts, `releaseDate` carries the start date and `dueDate` the end date.

With `includeRaw`, the `raw` object carries the source-derived columns under their portal names. They are source-derived rather than a byte copy: leading and trailing whitespace and the file's byte order mark are trimmed, and a title reassembled from an over-wide row may differ from the portal display. Use it to audit the normalization, not as a substitute for the portal record.

Dates are ISO 8601. Solicitation timestamps keep the published New York wall time with its offset (`2026-08-27T14:00:00-04:00`); contract rows publish no time of day, so they arrive as plain calendar dates (`2026-08-27`) rather than an invented midnight. Missing values are `null`, never an empty string. Records are deduplicated on `id`, keeping the later-dated copy, and ordered most recently published first, the same order the portal itself uses.

### Input

Everything is optional. Run it with no input and you get the newest solicitations.

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `mode` | enum | `solicitations` | `solicitations` or `contracts`. |
| `keyword` | string | - | Case-insensitive substring across title, agency and record number. |
| `agency` | string | - | Case-insensitive substring on the agency name, e.g. `PARKS`. |
| `method` | string | - | Case-insensitive substring on the procurement method, e.g. `RFI`, `M/WBE`, `Sole Source`. |
| `category` | string | - | Case-insensitive substring on the industry category or the sub-category. |
| `releasedAfter` | ISO date | - | Solicitation release date, or contract registration date, on or after this. |
| `dueAfter` | ISO date | - | Solicitation due date, or contract end date, on or after this. |
| `statuses` | string\[] | - | Exact, case-insensitive status match, e.g. `["Released", "Planned"]`. |
| `maxResults` | integer | 1000 | 1 to 20000. This is your cost cap: you pay per delivered record. |
| `includeRaw` | boolean | false | Attach a `raw` object with the source-derived columns (see the note above). |

#### Example inputs

**Open NYC RFPs and bids in your industry, due from today** (BD alert):

```json
{
  "mode": "solicitations",
  "category": "IT Related",
  "statuses": ["Released"],
  "dueAfter": "2026-08-23",
  "maxResults": 200
}
```

**M/WBE noncompetitive small purchase opportunities** (certified M/WBE firms):

```json
{
  "mode": "solicitations",
  "method": "M/WBE",
  "dueAfter": "2026-08-23",
  "maxResults": 500
}
```

**Who holds contracts with one agency, and for how much** (incumbent research):

```json
{
  "mode": "contracts",
  "agency": "PARKS",
  "releasedAfter": "2024-01-01",
  "maxResults": 2000
}
```

### Pricing

**$0.01 per record; you pay only for records delivered.** One charge per dataset item, nothing else. Set `maxResults` and you have set your bill: 100 records is $1.00. Filters are applied before delivery, so a narrow search costs less than a broad one.

### Data source, refresh and terms

Records come from the City of New York's PASSPort Public portal, from the same data files the portal's own browse pages load:

- Solicitations: `https://a0333-passportpublic.nyc.gov/dataJs/rfxData.js` (browse page: [rfx.html](https://a0333-passportpublic.nyc.gov/rfx.html))
- Contracts: `https://a0333-passportpublic.nyc.gov/dataJs/contractData.js` (browse page: [contracts.html](https://a0333-passportpublic.nyc.gov/contracts.html))

**Refresh:** The city regenerates these files itself, typically once a day; each run reports the file's `Last-Modified` timestamp in the run summary under the `OUTPUT` key. Run the Actor on a schedule and you track the portal's own refresh. There is no faster feed available without a PASSPort vendor login.

**Terms:** Data is read from the City of New York's PASSPort Public portal, a public transparency site. This Actor is not affiliated with or endorsed by the City of New York. No login or account is involved in reading these files. Buyers are responsible for their own use of the data; see the City's [terms of use](https://www.nyc.gov/main/terms-of-use). Field definitions and the City's own coverage caveats: [PASSPort Public Data Info](https://a0333-passportpublic.nyc.gov/datainfo.html).

The Actor makes exactly one request per run for one file, sends a descriptive User-Agent, opens no parallel connections, and caches nothing between runs.

### Limitations

- **PASSPort only, and only since January 2020.** The city's disclaimer is explicit that this covers mayoral-agency procurement processed in PASSPort since the platform went live. It is not every NYC contract. For citywide spend, see [Checkbook NYC](https://www.checkbooknyc.com/).
- **No login-only content.** Bid documents, addenda, attachments, Q\&A, and award notices behind the vendor login are not included. Solicitation items link to the public record so you can open it yourself.
- **Contracts have no per-record deep link.** The portal's contracts table does not publish one, so contract items link to the contracts browse page. Solicitation items get a real deep link.
- **Fields are as published.** Blank cells in the source arrive as `null`; the city leaves `category`, `registrationDate` and certification fields empty on plenty of records. A small number of contract titles contain unescaped commas in the source and are reassembled before delivery; the run summary counts them.
- **The city's own test records are in the data.** A handful of rows are internal tests (a "Public Hearing Test" contract starting in 2098). They are published data, so they are delivered rather than silently filtered.
- **Historic records are included by default.** Use `statuses` or `dueAfter` to keep only live opportunities.

### FAQ

#### How do I find open NYC government RFPs and bids?

Run in `solicitations` mode with `statuses: ["Released"]` and `dueAfter` set to today. Add `category`, `agency` or `keyword` to narrow to your industry. Every solicitation carries a deep link to its PASSPort Public record.

#### What is NYC PASSPort?

The City of New York's procurement and sourcing system. PASSPort Public is its transparency portal, which publishes solicitations and registered contracts for mayoral agencies since the platform went live in January 2020. This Actor reads the same data files the portal's browse pages load.

#### Can I see which vendors hold NYC city contracts?

Yes. `contracts` mode returns awarded contracts with `vendor`, `awardAmount`, `currentContractAmount`, `totalEncumberedAmount`, `totalPaidAmount`, start, end and registration dates, and the vendor's certification type and corporate structure.

#### How do I track NYC M/WBE contract opportunities?

Set `method` to `M/WBE`. The portal labels the M/WBE Noncompetitive Small Purchase method's solicitations as `RFI (M/WBE)`, and the substring filter catches them.

#### How often is PASSPort Public data updated?

The city regenerates the files typically once a day. Each run reports the file's `Last-Modified` timestamp so you know which copy you received.

#### Does it include bid documents and addenda?

No. Those sit behind the PASSPort vendor login. Solicitation records carry the public deep link so you can open the record yourself.

### Related datasets

Other datasets by the same author, all pay-per-record with source, refresh cadence and licence stated on each listing:

**Nursing home and long-term care data**

- [Nursing Home Deficiency Citations (CMS) Scraper](https://apify.com/jserle/nursing-home-deficiency-citations)
- [Nursing Home Fines & Civil Money Penalties (CMS) Scraper](https://apify.com/jserle/nursing-home-penalties-fines)
- [Nursing Home Directory & Ownership (CMS Form 671) Scraper](https://apify.com/jserle/ltc-facility-characteristics)
- [Nursing Home Daily Nurse Staffing (CMS PBJ) Scraper](https://apify.com/jserle/cms-nursing-home-staffing-daily)
- [Nursing Home Change of Ownership (CMS SNF CHOW) Scraper](https://apify.com/jserle/snf-change-of-ownership-tracker)
- [New York Nursing Home Bed Availability (NYSDOH) Scraper](https://apify.com/jserle/ny-nursing-home-bed-census)

**Medicare provider enrollment data**

- [Medicare Revoked Providers List (CMS Revocations) Scraper](https://apify.com/jserle/medicare-revoked-providers)
- [Medicare Opt-Out Providers List (CMS Affidavits) Scraper](https://apify.com/jserle/medicare-opt-out-physicians)
- [Medicare Revalidation Due Date List (CMS) Scraper](https://apify.com/jserle/medicare-revalidation-due-leads)

**Public procurement and RFP feeds**

- [E-Rate Form 470 RFP Scraper (USAC School & Library Bids)](https://apify.com/jserle/erate-form-470-rfp-feed)

**Developer and AI test data**

- [Synthetic Invoice OCR Ground Truth Dataset Generator](https://apify.com/jserle/ocr-ground-truth-document-factory)

# Actor input Schema

## `mode` (type: `string`):

Which PASSPort Public table to read. Solicitations are current and historic bidding opportunities. Contracts are awarded contracts with vendor, amounts and dates.

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

Case-insensitive substring matched against the title, the agency and the solicitation or contract number. Example: security guard

## `agency` (type: `string`):

Case-insensitive substring matched against the agency name. Example: PARKS

## `method` (type: `string`):

Case-insensitive substring matched against the procurement method. Examples: RFI, M/WBE, Competitive Sealed Bid, Micropurchase, Sole Source

## `category` (type: `string`):

Case-insensitive substring matched against the industry category or the sub-category. Examples: Construction, Goods - IT Related, Human/Client Service, Cleaning

## `releasedAfter` (type: `string`):

ISO date, for example 2026-01-01. Keeps solicitations released on or after this date; for contracts it applies to the registration date, or the start date when the contract is not registered yet. Records with no such date are excluded.

## `dueAfter` (type: `string`):

ISO date, for example 2026-09-01. Keeps solicitations whose response due date has not passed that date; for contracts it applies to the contract end date. Records with no such date are excluded.

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

Keep only these statuses, matched exactly and case-insensitively. Solicitations: Planned, Released, Responses Received, Closed. Contracts: In Progress, Registered, Pending ACCO Approval and similar.

## `maxResults` (type: `integer`):

How many records to deliver, newest first. You are charged per delivered record, so this is also your cost cap.

## `includeRaw` (type: `boolean`):

Attach a `raw` object with the source-derived columns under their portal names. Whitespace-trimmed, and titles repaired from over-wide source rows may differ from the portal display. Useful for auditing the normalization; roughly doubles item size.

## Actor input object example

```json
{
  "mode": "solicitations",
  "maxResults": 100,
  "includeRaw": false
}
```

# Actor output Schema

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

One item per solicitation or contract delivered by this run.

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

Source file, row counts, delivered records and whether the charge limit stopped the 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 = {
    "mode": "solicitations",
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("jserle/nyc-passport-procurement").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 = {
    "mode": "solicitations",
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("jserle/nyc-passport-procurement").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 '{
  "mode": "solicitations",
  "maxResults": 100
}' |
apify call jserle/nyc-passport-procurement --silent --output-dataset

```

## MCP server setup

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

```

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/7G0RFZswsuL6UBXQC/builds/LG1btvghl1pbwDF37/openapi.json
