# IRS Form 990 Nonprofit Financials & Grantmaker Scraper (`groundplanes/irs-form-990-nonprofit-grantmaker-scraper`) Actor

Extract IRS Form 990 nonprofit financials, aggregate executive compensation, and foundation grantmaking data from official IRS bulk files: the EO Business Master File extracts, the SOI annual extracts, and the Form 990 e-file XML dataset, all resolved from IRS download and index pages at run start.

- **URL**: https://apify.com/groundplanes/irs-form-990-nonprofit-grantmaker-scraper.md
- **Developed by:** [Groundplane Labs](https://apify.com/groundplanes) (community)
- **Categories:** Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 nonprofit records

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

## IRS Form 990 Nonprofit Financials & Grantmaker Data Scraper

Extract IRS Form 990 annual financials, aggregate executive compensation, foundation grantmaking, professional fees, and balance sheet totals for US tax-exempt organizations, built only on official IRS bulk data.

Found a problem or need a field? Open an issue on the Issues tab; we respond quickly.

***

### What It Does

This actor downloads and stream-parses three official IRS datasets, resolved fresh from the IRS download and index pages at the start of every run (no dated URLs are hardcoded):

- **EO Business Master File (BMF) extracts:** one CSV per state, updated monthly by the IRS. Provides organization identity, NTEE code, subsection, address, tax period, and summary revenue and assets.
- **SOI annual extracts:** Statistics of Income extracts of Form 990, 990-EZ, and 990-PF filings (the newest filing year that publishes a Form 990 extract). Provides summary-grade financials.
- **Form 990 e-file XML dataset:** the IRS year index CSVs plus the TEOS XML batch zips for the newest two filing years. Provides full parsed 990 line items for electronically filed returns.

Each organization appears **once**, taken from its most recent tax period with data. When a parsed 990 XML filing is the newest record, the row carries full line items and `summaryFinancials: false`. When an organization appears only in the BMF or SOI summary extracts, the row falls back to those summary fields and `summaryFinancials: true`, so buyers are never misled about precision. Organizations with neither parsed nor summary data are excluded, and the exclusion count is stated in the run status.

**Executive compensation is aggregate only.** Form 990 Part VII lists named officers; this actor sums those amounts into a single `executiveCompensationTotalUsd` total and never emits a named individual's compensation.

***

### Who It's For

- **Philanthropic advisors and grantseekers:** find grantmaking foundations, review annual giving and asset holdings.
- **Wealth managers and planned giving officers:** screen nonprofit endowments and balance sheet strength.
- **B2B vendors and corporate partnerships:** target established nonprofits above a revenue threshold.
- **Researchers and reporters:** study nonprofit sector economics and executive compensation in aggregate.

***

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `searchKeyword` | string | `""` | Case-insensitive substring matched against the organization name. Empty matches all. |
| `states` | array | `["NY", "CA", "TX", "IL", "MA"]` | State codes whose BMF files are processed. Empty processes the four BMF regional files (all states). |
| `nteeCategoryId` | integer | `4` | NTEE major category: 1 Arts, 2 Education, 3 Environment and Animals, 4 Health, 5 Human Services, 6 International, 7 Public and Societal Benefit, 8 Religion, 9 Mutual Benefit, 10 Unknown. `0` means all categories. The id is matched against the first letter of the BMF NTEE code. |
| `cCodeSubsection` | integer | `3` | IRC 501(c) subsection. `0` means all subsections. |
| `minTotalRevenueUsd` | integer | `1000000` | Minimum revenue, checked against the revenue of the exact filing each row is taken from. |
| `maxItems` | integer | `1000` | Maximum organizations returned, highest revenue first. |

#### Canary Input

```json
{
  "searchKeyword": "",
  "states": ["MA"],
  "nteeCategoryId": 4,
  "cCodeSubsection": 3,
  "minTotalRevenueUsd": 1000000,
  "maxItems": 10
}
```

***

### Output Fields

One row per organization:

| Field | Type | Description |
|---|---|---|
| `ein` | string | IRS Employer Identification Number (9 digits). |
| `organizationName` | string | Legal organization name. |
| `nteeCode` | string / null | BMF NTEE code. |
| `subsectionCode` | string / null | IRC 501(c) subsection code. |
| `city` | string / null | City from the filing or BMF record. |
| `state` | string / null | Two-letter state abbreviation. |
| `zipCode` | string / null | ZIP code. |
| `latestTaxPeriod` | integer / null | Most recent tax period with data, `YYYYMM`. |
| `totalRevenueUsd` | number / null | Total revenue in USD. |
| `totalExpensesUsd` | number / null | Total expenses in USD (not published in the BMF, so null on BMF-only rows). |
| `totalAssetsUsd` | number / null | Total assets, end of year, in USD. |
| `grantsAndContributionsPaidUsd` | number / null | Grants and contributions paid, where the source form reports it (Form 990 and 990-PF; the 990-EZ and the BMF do not publish grants paid, so those rows carry null here). |
| `executiveCompensationTotalUsd` | number / null | Aggregate compensation of current officers, directors, and trustees. Aggregate only. |
| `outsideIndependentContractorFeesUsd` | number / null | Outside contractor and professional service fees (Form 990 Part IX lines 11a and 11d to 11g), summed. |
| `accountingAndLegalFeesUsd` | number / null | Accounting fees plus legal fees (Form 990 Part IX lines 11b and 11c), summed. |
| `pdf990FilingUrl` | string / null | Official IRS PDF of the filing on apps.irs.gov, resolved per delivered row. Null when the IRS TEOS route returns no matching filing. |
| `summaryFinancials` | boolean | `true` when financials come only from the BMF or SOI summary extracts. |

#### Sample Output Row

```json
{
  "ein": "046103770",
  "organizationName": "EXAMPLE HEALTH FOUNDATION INC",
  "nteeCode": "E21",
  "subsectionCode": "3",
  "city": "Boston",
  "state": "MA",
  "zipCode": "02118",
  "latestTaxPeriod": 202412,
  "totalRevenueUsd": 12500000.0,
  "totalExpensesUsd": 11200000.0,
  "totalAssetsUsd": 85000000.0,
  "grantsAndContributionsPaidUsd": 6500000.0,
  "executiveCompensationTotalUsd": 450000.0,
  "outsideIndependentContractorFeesUsd": 120000.0,
  "accountingAndLegalFeesUsd": 45000.0,
  "pdf990FilingUrl": null,
  "summaryFinancials": false
}
```

The sample row above is synthetic and illustrates the schema only.

***

### How Pricing Works

Pay per result. You only pay for rows returned. Set `maxTotalChargeUsd` in your run options to cap a run's cost; a run stopped by that cap reports PARTIAL with the count delivered.

***

### Data Sources & Terms

All records come from IRS public-domain bulk data at irs.gov and apps.irs.gov: the EO BMF extracts (monthly), the SOI annual extracts (annual), and the Form 990 e-file XML dataset (posted monthly by filing year). No third-party nonprofit database or API is used. This tool is independently developed and is not affiliated with or endorsed by the Internal Revenue Service.

### Limits & Troubleshooting

- **Run time and size:** the actor downloads the BMF files for the requested states (about 1 to 20 MB each), the SOI extracts (about 80 MB total), the two newest 990 index files (about 17 MB each), and only the XML batch files (about 70 to 105 MB each) that contain filings for organizations that pass your filters, newest filing year first, stopping once every delivered organization has parsed XML data or the batches run out. Wide filter sets with a large `maxItems` can take tens of minutes and several GB of downloads.
- **Zero matches:** a healthy source with no matching organizations ends as `Complete: 0 organizations matched your filters`, with the scanned counts stated. Broaden the keyword, lower `minTotalRevenueUsd`, or add states.
- **PARTIAL runs:** a truncated or missing file in the resolved set, a corrupt XML filing, or unrecognized amount formats produce a PARTIAL status naming the file or the count. A PARTIAL caused only by your `maxItems` or charge cap carries no Issues line; a PARTIAL with a source cause does.
- **Stale data:** BMF posting dates are monthly, SOI extracts are annual, and 990 XML batches are posted monthly; `latestTaxPeriod` on each row tells you exactly how current that organization is.
- **990-EZ and BMF field gaps:** grants paid and total expenses are not published for every form type; affected fields are null on those rows rather than estimated.

### Changelog

- **v0.1.3** (2026-11-02): Fix round from the Red Team final GO review of v0.1.2. Professional fundraising fees reported under the current-schema Part IX line 11e group element FeesForServicesProfFundraising are now parsed into outsideIndependentContractorFeesUsd (that tag was missing from the contractor fees group map, so the field was understated, and sometimes null, on current-schema filings that report the fee only under that spelling). The TEOS PDF lookup now prefers a Form 990 item when TEOS lists both a 990 and a 990T for the same tax period, instead of returning whichever period match is listed first; when no item matches the tax period the URL is still null. TEST_NOTES.md now documents verification for this round, and the shipped fixture suite (tests/test_fixtures.py) was added with fixtures proving both fixes.
- **v0.1.2** (2026-11-02): Fix round from Red Team review of v0.1.1. Executive compensation is no longer double counted on filings that use the nested Part VII schema (wrapper plus per-officer groups); each officer compensation element is counted exactly once, and the single-tag aggregate is still preferred when present. The minimum revenue filter is no longer applied at the BMF stage: it is checked only against the revenue of the filing each row is taken from, after enrichment, and those drops are counted in the status. Validation-skipped rows now end the run as PARTIAL naming the skip count, so Complete means all matched rows were emitted. The PDF filing URL is null when no TEOS item matches the row tax period (the first-item fallback is removed). Added default run options with a 50 USD charge cap and set the source build tag to audit. Status counts inside one message now use one matched count consistently, and a truncated PARTIAL reason list states the total number of reasons.
- **v0.1.1** (2026-11-01): Rebuilt on official IRS bulk data only (BMF, SOI extracts, 990 e-file XML resolved from IRS pages at run start). The v0.1 build's third-party nonprofit API integration was withdrawn because that source's terms bar paid resale use. Also fixed in this round: accounting and legal fees are now summed instead of taking one or the other; organization identity is read from the ReturnHeader Filer block only, never from the preparer firm block; Part IX fee groups and the current-year grants tag are parsed in the current XML schema's group form; zero matches now end Complete instead of failing; the missing `.actor` schemas were restored; retries follow the 6-try standard with Retry-After honored.
- **v0.1** (2026-11-01): Initial release. Withdrawn the same day; see v0.1.1.

# Actor input Schema

## `cCodeSubsection` (type: `integer`):

IRC 501(c) subsection code, for example 3 for 501(c)(3). Set 0 for all subsections.

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

Maximum number of organizations to return, highest revenue first.

## `minTotalRevenueUsd` (type: `integer`):

Minimum total revenue in USD, checked against the revenue of the filing each row is taken from.

## `nteeCategoryId` (type: `integer`):

NTEE major category: 1 Arts, 2 Education, 3 Environment and Animals, 4 Health, 5 Human Services, 6 International, 7 Public and Societal Benefit, 8 Religion, 9 Mutual Benefit, 10 Unknown. Set 0 for all categories.

## `searchKeyword` (type: `string`):

Case-insensitive substring matched against the organization name (BMF NAME and SORT_NAME). Empty matches all names.

## `states` (type: `array`):

Two-letter US state codes whose BMF state files are processed. Empty processes the four BMF regional files (all states).

## Actor input object example

```json
{
  "cCodeSubsection": 3,
  "maxItems": 1000,
  "minTotalRevenueUsd": 1000000,
  "nteeCategoryId": 4,
  "searchKeyword": "",
  "states": [
    "NY",
    "CA",
    "TX",
    "IL",
    "MA"
  ]
}
```

# 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 = {
    "cCodeSubsection": 3,
    "maxItems": 1000,
    "minTotalRevenueUsd": 1000000,
    "nteeCategoryId": 4,
    "searchKeyword": "",
    "states": [
        "NY",
        "CA",
        "TX",
        "IL",
        "MA"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("groundplanes/irs-form-990-nonprofit-grantmaker-scraper").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 = {
    "cCodeSubsection": 3,
    "maxItems": 1000,
    "minTotalRevenueUsd": 1000000,
    "nteeCategoryId": 4,
    "searchKeyword": "",
    "states": [
        "NY",
        "CA",
        "TX",
        "IL",
        "MA",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("groundplanes/irs-form-990-nonprofit-grantmaker-scraper").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 '{
  "cCodeSubsection": 3,
  "maxItems": 1000,
  "minTotalRevenueUsd": 1000000,
  "nteeCategoryId": 4,
  "searchKeyword": "",
  "states": [
    "NY",
    "CA",
    "TX",
    "IL",
    "MA"
  ]
}' |
apify call groundplanes/irs-form-990-nonprofit-grantmaker-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,groundplanes/irs-form-990-nonprofit-grantmaker-scraper"
        }
    }
}
```

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/4PubkmmWdUYkl46dQ/builds/NLTZ1GWoIzkaNCfHd/openapi.json
