---
title: "Aggregate totals for a filter"
method: GET
path: "/api/v1/grants/summary"
tags: ["Grants"]
---

# Aggregate totals for a filter

`GET /api/v1/grants/summary`

## Request

- Base URL: `https://data.useplinth.com`
- URL: `https://data.useplinth.com/api/v1/grants/summary`
- Auth: API key in header `X-API-Key`

One call for the whole shape of a filter: total dollars, grant count, distinct funders and recipients, plus a year-by-year breakdown. Cheaper than paging the rows to count them.

## Query parameters

- `funder_id` string, nullable
- `recip_id` string, nullable
- `id_type` 'ein', nullable
- `year` string, nullable
- `min_year` string, nullable
- `max_year` string, nullable
- `subject` string, nullable
- `location` string, nullable
- `geo_id_type` 'state' | 'zip' | 'city' | 'county_fips' | 'country', nullable
- `location_type` 'recipient' | 'funder' | 'area_served', nullable
- `country` string, nullable
- `query` string, nullable
- `support` string, nullable
- `funder_type` 'private_foundation' | 'public_charity' | 'daf_or_passthrough', nullable
- `exclude` string, nullable
- `exclude_daf` string, nullable
- `min_amt` string, nullable
- `max_amt` string, nullable
- `min_match_prob` string, nullable
- `ein_source` string, nullable
- `jurisdiction` string, nullable
- `strict` string, nullable
- `format` 'json' | 'xml', nullable
- `population` string, nullable
- `transaction` string, nullable
- `include_gov` string, nullable
- `last_updated` string, nullable
- `profile_levels` string, nullable
- `example` '1' | 'trial' | 'unsupported' | 'list'

## Response `200`

Totals and a per-year breakdown over the whole filter.

- SummaryResponse: Aggregates over the WHOLE filter — not paginated, and not restricted by any trial row cap.
  - `code` integer
  - `message` string, required
  - `x_plinth` XPlinth: Non-Candid metadata. Safe to ignore for compatibility; `unsupported` is the one to assert on.
    - `warnings` ResponseWarning[], nullable
      - `parameter` string, required
      - `severity` string, required: `caveat` informs; anything else means a filter did NOT apply.
      - `reason` string, required
    - `unsupported` string[], nullable: Parameters that did not apply. Non-empty means the answer is not the question asked.
    - `dataset` DatasetStamp: Provenance of the slice that answered. `version` is the sync signal — poll it, re-pull when it moves.
      - `source` string, nullable
      - `refresh` string, nullable
      - `polled` string, nullable: How often we LOOK for a new IRS release, as distinct from how often the IRS publishes one.
      - `reporting_lag` string, nullable: Why a figure is dated to a fiscal year rather than to today.
      - `basis` string, nullable: Grants PAID versus grants approved — the reconciliation that catches people comparing a total here to a funder's own published giving figure. Read it before filing a discrepancy.
      - `version` string, nullable: Opaque build stamp of the data slice. Changes only when the data does.
      - `generated_at` string, nullable: ISO 8601 timestamp the slice was built.
      - `license` string, nullable: Licence the source data is published under, where it carries one. Absent for public-domain sources.
      - `attribution` string, nullable: Attribution the source licence obliges you to reproduce. Present it verbatim wherever you show figures from this jurisdiction.
      - `currency` string, nullable: Currency of every amount in the response, when it is not US dollars. Amounts are never converted — they are the filing's own figures.
    - `datasets` DatasetStamp[], nullable: Every corpus this response could have drawn on, each with its own licence terms. /sql only: you write the query, so which corpus answered is not knowable per response — honour every licence listed.
      - `source` string, nullable
      - `refresh` string, nullable
      - `polled` string, nullable: How often we LOOK for a new IRS release, as distinct from how often the IRS publishes one.
      - `reporting_lag` string, nullable: Why a figure is dated to a fiscal year rather than to today.
      - `basis` string, nullable: Grants PAID versus grants approved — the reconciliation that catches people comparing a total here to a funder's own published giving figure. Read it before filing a discrepancy.
      - `version` string, nullable: Opaque build stamp of the data slice. Changes only when the data does.
      - `generated_at` string, nullable: ISO 8601 timestamp the slice was built.
      - `license` string, nullable: Licence the source data is published under, where it carries one. Absent for public-domain sources.
      - `attribution` string, nullable: Attribution the source licence obliges you to reproduce. Present it verbatim wherever you show figures from this jurisdiction.
      - `currency` string, nullable: Currency of every amount in the response, when it is not US dollars. Amounts are never converted — they are the filing's own figures.
    - `excluded` object, nullable: Per-class cost of `?exclude=`, keyed by class name, plus `overlapping` — true when classes were counted independently and therefore sum to more than the whole. /grants/summary only.
    - `unidentified` Unidentified: Grants held OUT of a recipient ranking because their recipient is not an organization.
      - `grants` integer, nullable
      - `dollars` integer, nullable
      - `note` string, nullable
      - `unavailable` string, nullable: Present only when the size could not be computed. The ranking above is unaffected.
  - `summary` SummaryTotals, required: Corpus totals for the filter. ⚠️ NO `recipient_count`, DELIBERATELY (removed 2026-08-19). It was two incommensurable things added together — 961,743 resolved EINs plus 1,721,550 distinct strings a filer typed — so 64% of it counted text rather than organizations and the total had no defined unit. Worse, it moved the WRONG WAY: resolving a name to an EIN merges it with that EIN's other rows, so the figure FALLS as entity resolution improves. Resolution has gone 67.3% → 76.0% while this was published, which can only have driven it down against an unchanged reality. It also cost more than everything else here combined: profiled on the live warehouse, the other five fields are ~1.2s and this one was ~6s (a COUNT(DISTINCT) over 2.68M keys, most of them long strings, wrapped in a per-row regexp and a correlated EXISTS). Measured alternatives, both rejected: approx_count_distinct returned 2,469,271 against a true 2,683,293 (8% wrong, not publishable), and precomputing the normalized key as a Parquet column bought 1.44x for +112MB. A per-FUNDER recipient_count still exists on /grants/funders and /premier and is untouched: at that scale it is grouped, cheap, and interpretable, and lib/challenge.ts answers a trial-key challenge from it.
    - `total_amount` integer, nullable
    - `grant_count` integer, nullable
    - `funder_count` integer, nullable
    - `first_fiscal_year` integer, nullable
    - `last_fiscal_year` integer, nullable
  - `by_year` SummaryByYear[], required
    - `fiscal_year` integer, nullable
    - `total_amount` integer, nullable
    - `grant_count` integer, nullable
    - `funder_count` integer, nullable

## Other responses

- `401`: No API key, or a key we do not recognize.
- `402`: The call allowance is spent, the account is inactive, or the endpoint needs a paid plan — the message says which, and names the period it resets on (daily on free and non-profit plans, monthly on Pro).
- `422`: Validation Error

---

[API](https://skmtc.dev/useplinth/apis/plinth-grants-api.md) · [All operations](https://skmtc.dev/useplinth/apis/plinth-grants-api/llms.txt) · [OpenAPI document](https://skmtc.dev/useplinth/apis/plinth-grants-api/revisions/15273cd72ecf?raw)
