Skip to content
Plinth

For developers

A free alternative to the Candid API

Plinth publishes a JSON grants API over 211,000 US grantmakers and 20.1 million grants, read from IRS Form 990, 990-EZ and 990-PF filings. It is free with a key at 50 calls a day for the grant graph, and it deliberately follows the parameter vocabulary of existing grant-data APIs, so porting a client is mostly mechanical. If you are replacing Charity Check rather than the Grants API, compliance screening and the organization profiles are on a paid key here, and the differences are set out below.

Where this comparison comes from. Everything on this page is taken from Candid’s published documentation and from the public IRS Form 990 e-file data that both services read. Where their documentation does not settle a question, we say so rather than guess, and figures attributed to Candid are cited to the page that states them.

It is not a drop-in replacement, and this page is specific about why. Candid figures below were read from Candid’s own developer portal and pricing pages on 12 August 2026.

In one screen

Doing research? Use SQL instead

The migration guide below is accurate: most parameters map, and a client that pages through grant records will move across with small changes.

But if you are doing analysis rather than replacing an integration, don’t page through REST at all. POST /api/v1/sqlruns read-only SQL over the whole corpus: grants, funders, co-funder overlap, board interlocks and government awards, already joined. “Which funders back the same grantees as this one, and who funded them first” is one query there and dozens of paged calls otherwise. Candid has no equivalent.

Coverage and accuracy figures for the matching underneath are on the data-quality page.

What changes in a migration

One edit is unavoidable. Two more apply only if you use the parameters concerned; the second changes no syntax at all.

1 · The authentication header

Candid authenticates with Subscription-Key. We accept X-API-Key or an Authorization: Bearer token. Keys start plinth_sk_ and are secrets, keep them server-side. Amount filters need no change at all: min_amt and max_amt mean the same thing on both.

# before
curl -H 'Subscription-Key: $CANDID_KEY' \
  'https://api.candid.org/grants/v1/transactions?recip_id=13-1837418&id_type=ein'

# after
curl -H 'X-API-Key: $PLINTH_API_KEY' \
  'https://data.useplinth.com/api/v1/grants/transactions?recip_id=13-1837418'

2 · Two parameters share a name and not a meaning

These are the ones that survive a find-and-replace on the hostname and then quietly answer a different question. Both now refuse a Candid-shaped value rather than returning an empty page you would read as a real finding.

They are subject, where Candid’s is a PCS code and ours is NTEE, and location, which Candid reads as a GeoNames id and we read as a US state by default. The exact accepted values for both, and for the three other parameters that need a converted value (id_type, geo_id_type, sort_by), are in the parameter table below.

3 · 5 filters have no counterpart, and say so

We accept every one of them rather than rejecting the request, and name each in the response. A migrated query that silently drops population=children returns valid-looking output over every population; that is how a hostname change turns into wrong research. Add strict=1 and the same request is a 400 instead. Use it in your test suite so a lost filter fails loudly rather than at read time.

GET https://data.useplinth.com/api/v1/grants/summary?population=children&year=2023

{
  "code": 200,
  "summary": { ... },
  "x_plinth": {
    "unsupported": ["population"],
    "warnings": [{ "parameter": "population", "reason": "PCS population ... " }]
  }
}
# response header: X-Plinth-Unsupported: population

They are include_gov, population, transaction, last_updated and profile_levelseach with the reason it cannot be honored, and the nearest thing we do have, in the parameter table below.

Endpoint mapping

CandidPlinthNotes
GET /grants/v1/transactionsGET /api/v1/grants/transactionsNo changeIndividual grant rows. Same four-resource model.
GET /grants/v1/fundersGET /api/v1/grants/fundersNo changeFunder-side aggregates.
GET /grants/v1/recipientsGET /api/v1/grants/recipientsNo changeRecipient-side aggregates.
GET /grants/v1/summaryGET /api/v1/grants/summaryNo changeTotals for a filter, plus a by_year breakdown Candid has no counterpart for.
POST /essentials/v4POST /api/v1/essentialsNo changeNonprofit SEARCH. Accepts Candid's search_terms / from / size / filters / sort body shape; the filter vocabulary differs (see the essentials matrix). GET /api/essentials/{ein} remains the single-record lookup.
GET /premier/v4/{ein}GET /api/v1/premier/{ein}ConvertSame concept, different depth. Filing-derived financials, geography, grantmaking, funders and (Pro) governance roster; no nonprofit-supplied profile, DEI or impact data. The response names what is absent under x_plinth.not_available.
GET /charitycheck/v1/{ein}GET /api/v1/screening/{ein}ConvertSame IRS sources (BMF, Pub 78, auto-revocation), every code decoded, plus the three screens Charity Check also runs: OFAC (SDN AND Consolidated, primary AND alias names, where Candid screens the SDN list), the Internal Revenue Bulletin §170 deletion announcements, and California registration (AG Registry + FTB revoked list). `compliance.reliance` additionally carries the Rev. Proc. 2018-32 §8.01 elements element by element — including the EO BMF Extract revision date and whether it is the current one — and flags the §6.02 and §6.03 cases where reliance does not apply at all. A screen whose source is unavailable reports `not screened`, never `clear`.
Charity Check Bulk (≤25 EINs)POST /api/v1/screening/bulkPlinth onlySame idea, 100 EINs per call rather than 25.
Charity Check PDFGET /api/v1/screening/{ein}?format=pdfConvertA one-page retainable report carrying the Rev. Proc. 2018-32 §8.01(1) elements, every screen with whether it ran, and the revision date of each source file. Rendered from the same response object the JSON route returns, so the document and the API cannot disagree. Where the IRS files conflict it names the governing section rather than picking a side — a revocation record beside a current listing is §4.06, and the report prints the dates the comparison turns on plus the steps to settle it against the determination letter, which is in no bulk extract. Every report carries an identifier, a format version and, where a signing key is configured, an Ed25519 signature; `POST /api/screening/verify` checks a retained one and is keyless. §8.01(2) puts retention on the grantor. Paid keys only.
Charity Check CaliforniaGET /api/v1/screening/{ein} → state_registration.californiaConvertCovered, from the same two state sources: the AG's Registry of Charitable Trusts (may it solicit; is its reporting current) and the Franchise Tax Board's revoked list (has California revoked its exemption, whatever the IRS says). Returned inline on the compliance record rather than as a separate endpoint. California only — no other state registry is screened.
Taxonomy API (PCS)—Not supportedPCS is Candid's proprietary taxonomy. We classify on NTEE, which is the IRS's and is public.
—POST /api/v1/sqlPlinth onlyRead-only SQL over the whole warehouse.
—GET /api/v1/searchPlinth onlyName → EIN resolution, keyless and unmetered, including semantic matching.
Charity Check IRB screenGET /api/v1/screening/{ein} → irb_screenConvertInternal Revenue Bulletin §170 deletion announcements, read from the bulletins themselves. Name/city/state only — the announcements carry no EIN — so it is a name match, and `covers_through` names the newest bulletin read so a 'clear' result has a date attached. An IRB listing is a diligence flag, not a determination, and the response says so.
—POST /api/v1/screening/verifyPlinth onlyChecks a retained compliance report: recomputes its identifier, verifies its signature against the published Ed25519 key, and reports whether the format version it was issued under is still current. Keyless and unmetered — confirming a filed document should not spend a screen, and the auditor doing the checking is the person least likely to hold a key. An unverifiable signature reports null, never false.

Candid endpoint names and parameters read from the Grants API reference and authentication docs, 2026-08-12.

Every Grants parameter, one by one

10 carry over unchanged, 6 need a converted value, 5 have no counterpart and are reported at runtime, and 9 exist here and not on Candid. This table is generated from the same file the API builds its filters and warnings from, so it cannot drift from the behavior, and the machine-readable form is at /api/v1/meta.

CandidPlinthNotes
funder_idfunder_idNo changeEIN. Any format — hyphens and missing leading zeros are normalized. Comma-separate for several.
recip_idrecip_idNo changeEIN, same normalization. Comma-separate for several.
id_typeid_typeConvertOnly `ein` is accepted; Candid's proprietary organization keys have no meaning here. Any other value is rejected with a pointer to /api/search, never silently mis-filtered.
yearyearNo changeFiscal year. Comma-separated lists and `2019-2023` ranges are both accepted.
subjectsubjectConvertSAME NAME, DIFFERENT TAXONOMY — the single most dangerous parameter in a migration. Candid's `subject` is a PCS code; ours is NTEE. We accept an NTEE major letter (`T`), a full NTEE code (`T31`), or the cause label (`Philanthropy`), and reject anything shaped like a PCS code rather than returning a plausible wrong answer.
locationlocationConvertCandid takes a GeoNames id; we take a US state code by default, or a ZIP, city, county FIPS or country code when `geo_id_type` says so. Comma-separate for several.
geo_id_typegeo_id_typeConvertCandid's value is `geonameid`. Ours selects how `location` is read: state | zip | city | county_fips | country. GeoNames ids are not resolved.
location_typelocation_typeNo changerecipient (default) | funder | area_served — the same three geographies Candid distinguishes, resolved from what the filings carry. Note the semantics of `area_served`: it selects FUNDERS whose giving reaches the place (their grant footprint, unioned with any service area we resolved from their filing and website) and returns all of their grants, including ones made elsewhere — the 'who funds work here, and what else do they do' question. For grants landing IN a place use location_type=recipient. Candid's area_served is a grant-level declared facet, so the two sets will differ; every response using it says so.
min_amtmin_amtNo changeWhole dollars.
max_amtmax_amtNo changeWhole dollars.
queryqueryNo changeFree-text keyword search. Matches the grant's as-filed purpose text and the recipient name. Candid searches its own curated description field, so recall differs even though the parameter is the same.
sort_bysort_byConvertamount, year (Candid's `year_issued` is accepted as an alias), count, funder_name, funder_state, recip_name, recip_state — whichever the endpoint can order by. An unsupported field is reported, not silently swapped.
sort_ordersort_orderNo changeasc | desc.
pagepageNo change1-based.
formatformatNo changejson (default) | xml, as Candid's Grants endpoints offer.
include_govinclude_govNot supportedThere are no government grantmakers to include or exclude: this corpus is built from Form 990 filings, and government agencies do not file them. `include_gov=false` is therefore already the state of the data. Use /api/premier gov_funding, or the gov_funding_* tables over /api/sql, for money flowing FROM government.
population—Not supportedPCS population (who a grant benefits) is Candid-only classification work; 990 filings do not carry it and we do not infer it. Approximate with `subject` plus `query`, and expect lower precision.
transaction—Not supportedPCS transaction type. Effectively every row here is a cash grant as reported on Schedule I / Part XIV.
last_updated—Not supportedNo per-row change timestamp. The dataset is versioned as a whole — read `x_plinth.dataset` on any response, or GET /api/meta, and re-sync when the build stamp moves.
profile_levels—Not supportedCandid/GuideStar profile participation level. Nothing equivalent exists outside Candid's own platform.
—limitPlinth onlyPage size up to 1,000, against Candid's fixed 25.
supportsupportConvertCandid's `support` takes a PCS support-strategy CODE; ours takes a label derived from the filing's own purpose text (see supportStrategies). Same parameter NAME, different vocabulary — exactly what `transformed` means here: the caller must convert the value. Recall-oriented and openly approximate: it finds grants whose stated purpose says so, not a licensed classification.
—countryPlinth onlyRecipient country for cross-border giving, as the FIPS-10-4 code the 990 foreign-address block carries (India='IN', United Kingdom='UK').
—funder_typePlinth onlyprivate_foundation | public_charity | daf_or_passthrough — separates institutional money from donor-advised pass-through.
—excludePlinth onlyDrop structurally distorting flows, comma-separated: `daf` (donor-advised and pass-through conduits), `individuals` (grants to people rather than organizations — patient assistance, scholarships, disaster relief), `related_party` (transfers inside one economic entity — an endowment trust paying its foundation, a supporting organization paying its parent). Together about half of corpus dollars — the exact share is recomputed per rebuild (pipeline/candid_compat.EXCLUDE_SHARE_OF_DOLLARS) because it moves with related_edge; it read 43.7% here while the live service answered 50.9%. Nothing is excluded by default. A total that drops them and a total that keeps them are different claims, so the caller makes it; `x_plinth.excluded` reports what each class cost against your own filter. This is the superset of `exclude_daf`, and the only way to drop the two distortions that survive it — both of which the amount-ranked caveat names.
—exclude_dafPlinth onlyDrop donor-advised-fund sponsors and other pass-through vehicles. Exactly equivalent to `exclude=daf` and folded into the same filter, kept because it is the parameter the amount-ranked caveat names and the one a caller will already have found in the docs. Reach for it whenever you rank by dollars: sponsors report enormous totals because money passes through them from individual donors, so an amount-ranked list is topped by them unless you say otherwise. Use `exclude=` when you also want the in-kind and related-entity distortions dropped.
—min_year / max_yearPlinth onlyOpen-ended fiscal-year bounds.
—min_match_probPlinth onlyKeep only rows whose recipient EIN carries `x_plinth.match_prob` at or above this value. Rule-resolved rows carry their rule's measured precision, model-resolved rows a per-row calibrated probability that tops out near 0.965; a threshold above that drops every model row and the response says so.
—ein_sourcePlinth onlyKeep only rows whose recipient EIN came from the named resolution rules (comma-separated): filed, override_human, override_keyerror, name, registry-dominant, fuzzy, override_llm, registry, address, zip, street, model. `filed` is the filer-stated subset.
—strictPlinth onlystrict=1 turns any unsupported or unrecognized parameter into a 400 instead of a warning — the setting to use in a migration test suite so a dropped filter fails loudly. It does NOT fire on caveats (a filter we applied correctly and are being honest about, like `support` being text-derived): 400ing on those would make strict unusable, teams would switch it off, and they would stop seeing the warnings that do matter. `x_plinth.warnings` carries `severity` so you can tell them apart; `x_plinth.unsupported` lists only the fatal ones.

Essentials is a search API

Candid’s POST /essentials/v4 searches across organizations. Mapping it onto GET /api/essentials/{ein}, a single-record fetch, would turn a search into a lookup, so we serve POST /api/essentials in the same body shape: search_terms, from, size, filters, sort. The filter vocabulary inside is ours, because Candid’s is built on PCS and on profile data we do not hold.

curl -X POST -H 'X-API-Key: $PLINTH_API_KEY' \
  -H 'content-type: application/json' \
  'https://data.useplinth.com/api/essentials' -d '{
    "search_terms": "food bank",
    "size": 25,
    "filters": { "state": "MA", "min_revenue": 1000000, "pub78_verified": true },
    "sort": { "field": "total_revenue", "order": "desc" }
  }'
CandidPlinthNotes
search_termssearch_termsNo changeOrganization name / keyword search. `mode: semantic` additionally matches on mission meaning.
from / sizefrom / sizeNo changesize up to 1,000, against Candid's 25.
filters.geography.statestateNo changeTwo-letter code, or a comma-separated list.
filters.geography.city / zipcity / zipNo changeFrom the BMF address.
filters.financials revenue / assets / expenses rangesmin_revenue / max_revenue / min_assets / max_assets / min_expenses / max_expensesNo changeFrom the organization's own latest filing rather than Candid's compiled figures.
filters.organization.subject (PCS)cause / nteeConvertNTEE major group or code, not PCS.
filters.organization.pub78_verifiedpub78_verifiedNo changePresent on the current Publication 78 list.
filters.organization.form_typesreturn_typeNo change990 | 990EZ | 990PF | 990T.
filters.organization.profile_level—Not supportedCandid platform metadata.
filters.organization.demographics—Not supportedNonprofit-supplied demographic data is Candid's Demographics API.
—subsectionPlinth only501(c) subsection code, e.g. 03.
—foundation_typePlinth onlyPC | PF | POF | SO — the IRS foundation classification.
—revokedPlinth onlyFilter on IRS automatic-revocation status.
—min_grants_paidPlinth onlyOnly organizations that actually make grants at some scale.

Support strategy, without the PCS license

Candid’s PCS supportfacet answers “what is the funder paying for?”: unrestricted operating money versus a restricted project. That is a genuinely useful prospecting signal and we cannot license the classification, so we derive it from the grant’s own purpose text as filed. It is approximate by construction: it finds grants that say so, and misses those that fund the same way without saying it. Every response using it repeats that caveat.

general_operating

General operating support

Unrestricted funding for the organization's day-to-day work.

program

Program support

Funding earmarked for a named program or project.

capital

Capital support

Buildings, renovation, land and other capital projects.

endowment

Endowment

Gifts to endowment or a permanently restricted fund.

scholarship

Scholarships and student aid

Scholarships, tuition assistance and student awards.

fellowship

Fellowships and prizes

Fellowships, residencies, prizes and individual awards.

research

Research

Research, evaluation and study.

capacity_building

Capacity building

Organizational strengthening, technical assistance, training.

matching

Matching and challenge grants

Grants conditioned on funds raised elsewhere, including employee-match programs.

emergency_relief

Emergency and disaster relief

Disaster response, emergency and humanitarian relief.

equipment

Equipment and technology

Purchase of equipment, vehicles or technology.

sponsorship

Sponsorship and events

Event sponsorship, galas, table purchases and memberships.

What is not equivalent

Enrichment beyond the filings. Candid states Premier carries 760+ additional fields, much of it collected directly from nonprofits through profile claiming and Seals of Transparency. That data does not exist in IRS filings, so we do not have it at any price.

Products with no counterpart. News, Taxonomy, Demographics, Nonprofit Eligibility and Open RFP Opportunities. Candid’s Demographics and Taxonomy APIs are listed at no fee, so “free API” is not by itself a reason to switch.

Corpus size. Candid states 29 million grants and 304,000 funder profiles, against our 20.1 million grant rows and 211,000 funders. Of ours, 15.2 million are linked to a specific recipient organization; the rest are real grants whose recipient name we would not stake an EIN on, so they carry no classification and drop out of any recipient-keyed filter. We publish that split, and the measured precision behind it, on /data-quality. If coverage is the binding constraint, Candid has more.

Why the two numbers differ. They are not counting the same thing. Candid’s data-sources page (2026-08-15) lists funders who report to Candid directly, organization websites, news and press releases, funder-network partnerships, and UK grantmaking published to the 360Giving standard. Ours is one source: US e-filed Form 990, 990-EZ and 990-PF, FY2017–2023. That is the whole trade. Their number reaches money ours cannot see: grants a funder announced but has not yet filed, non-US giving, funders who volunteer data and every row of ours resolves to a named filing you can open and check, with no row resting on a press release. Neither is the better number in the abstract; they answer different questions, and the one you want depends on whether you need reach or receipts.

What we checked, and what we found. Candid released Essentials and Premier v4 on 25 March 2026. We have since worked through their published v4 field reference line by line, and the mapping above reflects it. One divergence was material enough to build for: Candid types several booleans as the strings "True" and "False". We return real JSON booleans by default, and emit Candid’s rendering on request. Add ?compat=legacy_stringsto a compliance call and an existing parser keeps working. The one caveat left: Candid’s published specification index lists Essentials and Premier at v1.0 rather than v4, so the v4 shapes are read from its live reference rather than a versioned spec document. Check response fields against your own usage before you cut over.

The path is versioned. Candid has /grants/v1/transactions; ours is /api/v1/grants/transactions. Inside v1we add and do not take away — new fields can appear, so parse permissively, but nothing is removed, renamed or redefined under a client that is already running. The unversioned /api/grants/…still resolves and will keep resolving; pin to /api/v1 for anything you deploy.

Our specification is published, and generated. /openapi.jsonis OpenAPI 3.1 built from the service’s own route signatures rather than maintained by hand, so it cannot describe an endpoint the service does not have (it is edge-cached, so a redeploy takes a few minutes to show) — point a client generator, an HTTP client or an agent straight at it. /llms.txt indexes the documentation for the same purpose, and /.well-known/apis.json carries the machine-readable description. Every parameter we accept, and every one we deliberately do not, is in there.

What you get that Candid has no counterpart for

A SQL endpoint. /api/v1/sql runs analytical queries directly against the corpus, so questions that would take many paged REST calls become one query.

Plain-English querying. /api/v1/analyze takes a question, writes and runs the query, and returns a sourced answer with the SQL it used. Also available in the browser at /analyze.

Pages of 1,000, not 25. Candid caps its Grants endpoints at 25 results a call. Ours go to 1,000, and every response carries results_count and total_pagesso a pager does not have to derive them. Bulk compliance takes 100 EINs against Candid’s 25, and costs one call, not one per organization.

Both directions of the funding graph on the organization record. /api/premier answers who an organization funds and who funds it, with the top funders named — a Premier profile describes the organization, not the money reaching it.

Filters built on our own classification work — donor-advised and pass-through funders separable from institutional money (funder_type), the three classes of flow that distort any total separable in one parameter (exclude=daf,individuals,related_party — pass-through money, grants to people rather than organizations, and transfers inside one economic entity, together 50.9%of corpus dollars), cross-border giving by country from the 990 foreign-address block, and support strategy from the filing’s own wording.

Layers computed across the whole corpus — co-funder overlap strength, portfolio clustering, board interlocks from Form 990 Part VII, and federal plus nine-state government funding joined to the same organizations.

What the compliance endpoint does

The full write-up of how the screen works, including the two kinds of join and the rule that a missing source reports not screened rather than clear, is on /screening. What follows is the Candid-shaped mapping.

GET /api/screening/{ein} returns one JSON record covering seven reference files from four agencies. POST /api/screening/bulk runs identical logic over up to 100 EINs for one call from your allowance. Both are on a paid key; every key gets a metered taste of the single screen with the evidence fields blanked and named.

Federal status, decoded

From the IRS Business Master File and Publication 78, returned as descriptions rather than raw codes: exemption subsection, foundation classification with its 509(a)paragraph and PC/PF/POF/SO bucket, deductibility, exempt status, ruling date, filing requirement, legal form, affiliation, NTEE, and the BMF’s own financial summary. Group subordinates are resolved to their central organization. The auto-revocation list is reported with its revocation, posting and reinstatement dates, so a reinstated organization does not read as revoked.

Where the BMF shows an active 501(c)(3) and Pub 78 does not list the EIN, that is returned as irs_bmf_pub78_conflict— a needs-review flag with the reason, not a boolean that picks a side. The two files refresh on different schedules and do diverge.

Four screens beyond status

OFAC, all four files. SDN and Consolidated, primary and AKA/FKA/NKA names. Screening SDN primary names alone is a false-negative machine — OFAC lists most entities under several names, and the Consolidated list is separate again. Each response names which of the four it reached.

Internal Revenue Bulletin. The §170 deletion announcements, read from the bulletins. covers_throughnames the newest bulletin read, so a clear result carries a date. A listing is a diligence flag, not a determination — the IRS generally does not disallow contributions made on or before the announcement date — and the response says so.

California, both halves. The Attorney General’s Registry of Charitable Trusts (may it solicit; is its reporting current) and the Franchise Tax Board’s revoked list (has California revoked its exemption, whatever the IRS says). The halves are tracked separately, so one being unavailable reports partially screened rather than a clean result.

Auto-revocation as above, screened by EIN rather than by name.

The reliance elements

compliance.relianceanswers the IRS’s own checklist for relying on third-party EO BMF data — Rev. Proc. 2018-32 §8.01 — element by element, with the evidence for each:

§8.01(1)(a) name, EIN, §509(a) status including supporting-organization type, and whether contributions are deductible. §8.01(1)(b) the EO BMF Extract’s revision date — thepublisher’s date, which is a different fact from when we downloaded it — plus whether that is the current extract, established by asking the IRS host rather than assumed. §8.01(1)(c) the date and time the record was provided. Where an element cannot be evidenced it reports absent, never a substitute value.

It is scoped to the BMF, explicitly. §8 governs foundation status and deductibility and says nothing about sanctions, the IRB or state registration; those screens are diligence, and no revenue procedure makes them a safe harbor.

It also computes the cases where reliance does not apply at all, which a clean-looking BMF row will not tell you: §6.02, an organization not yet listed in either file, and §6.03, a subordinate covered by a group exemption letter — regardless of how it appears in the EO BMF Extract.

Where the IRS files disagree

They do disagree. An organization can sit on the Auto-Revocation List and be listed on Publication 78 with an active BMF status at the same time, because those files move on different schedules and a reinstatement reaches them at different moments. A screen that returns a single verdict has to guess, and both guesses are wrong: call it revoked and you tell a grantmaker not to fund an organization the IRS has re-recognized; call it clear and you miss a live revocation.

So the report names the governing rule instead. That pattern is Rev. Proc. 2018-32 §4.06, which turns on which came later, and the report prints the dates the comparison needs — revocation date, Auto-Revocation posting date, EO BMF ruling date — and says which of §4.06’s two limbs it could evaluate and which it could not.

And it says what to do next, specifically. The fact that settles §4.06 is the effective date on the determination letter, which lives in IRS Tax Exempt Organization Search and in no bulk extract — so no product screening the bulk files, ours or anyone’s, can establish it. Rather than leave that as “check TEOS”, the report carries numbered steps: which database, which date to read, what to compare it to, what to do when the letter predates 2014, and that the result should be filed alongside the report.

The report

?format=pdfreturns the same record as a one-page dated document to file with the grant record: the §8.01(1) elements, every screen with whether it ran, the revision date of each source file, and the limitations that apply to that organization. It is rendered from the response object itself, so the document and the API cannot disagree. §8.01(2) puts retention on you — we keep no copy.

Each one carries three things a retained document needs. An identifier, a digest of the record, its sources and the moment it was provided: re-derive it from the response you kept and it reproduces; alter the response and it does not. A format version, because what a report asserts changes as the reasoning improves, and a holder of last quarter’s copy is entitled to know whether today’s would say something different. And an Ed25519 signature where a signing key is configured — the identifier evidences drift, the signature answers whether it came from us.

POST /api/screening/verify checks a retained report and is keyless and unmetered: confirming a document already in a grant file should not spend a screen, and the auditor doing the checking is the person least likely to hold a key. It reports whether the identifier matches, whether the signature is valid, and whether the format is current — and an unverifiable signature comes back null rather than false, because “we could not check” and “it is forged” are different answers. This is a detached signature over the report data, not an X.509/PAdES PDF signature, so no signature panel appears in a PDF reader; the report says so on its face.

Freshness

Every source with a publisher timestamp is polled every fifteen minutes: a header request each, pulling the file only when its revision actually moves. The Internal Revenue Bulletin and the California AG Registry publish nothing to poll against, so they run daily and the response says so per source rather than quoting one figure for all seven.

Every response carries, per source, the publisher’s revision date, the date we pulled it, and whether it is the current one. What no third party closes is the gap between a sanctions designation taking effect and OFAC republishing its files — for a screen at the moment of disbursement, Treasury’s live search is the source of record.

The rule underneath all of it

A screen whose source is unavailable reports not screened, never clear. Seven files from four agencies on four schedules means one will be down some month, and a caller who reads an unavailable source as a clean result disburses money on a check that never ran. Every screen reports whether it ran, separately from what it found.

What the endpoint does not do is stated in x_plinth.not_available on every response: sanctioned individuals are not screened, only entities; every name-keyed screen matches the organization’s primary IRS name, so another trading name can be missed and a common name can collide; state registration covers California; and none of it is a determination. Plinth is not a law firm.

Where Plinth is the wrong tool

Get a key

Free, no card, about a minute: generate an API key. Full reference at /developers. If you are weighing the platforms rather than the APIs, Plinth vs Candid covers the product comparison, and the comparison hub covers the rest of the market.

Questions and answers

Is there a free alternative to the Candid API?
Plinth's grants API is free with a key at 50 calls a day, covering 211,000 US grantmakers and 20.1 million grants from IRS 990, 990-EZ and 990-PF filings, with traversal in both directions. Candid's grant and nonprofit data APIs are paid (the Grants API starts at $6,000 a year) though Candid offers a 30-day free trial on request and lists its Demographics and Taxonomy APIs at no fee. ProPublica's Nonprofit Explorer API is also free but returns Form 990 summaries with no grant-level records.
Can I point an existing Candid client at Plinth?
Closer than it was, but still not drop-in. One edit is unavoidable: the authentication header (Candid uses Subscription-Key; we use X-API-Key or a Bearer token). Amount filters do NOT change: both APIs use min_amt and max_amt. Two parameters share a name and not a meaning and must be converted: subject is a Candid PCS code and an NTEE group here, and location is a GeoNames id there and a state, ZIP, city, county FIPS or country code here. Five Candid filters have no counterpart at all: population, transaction, include_gov, last_updated and profile_levels. We accept all five rather than rejecting the request, and name them in x_plinth.warnings so a dropped filter is visible instead of silently widening your result set; add strict=1 and they become a 400 instead, which is what you want in a migration test. Candid moved Essentials and Premier to v4 in March 2026; we have since worked through their published v4 field reference line by line, and the one that bites is that their documentation types several booleans as the strings "True" and "False". We return real JSON booleans by default and render Candid's form on request — add compat=legacy_strings to a compliance call and your existing parser keeps working. Check the rest against your own usage: Candid's published specification index lists Essentials and Premier at v1.0, not v4.
What is not equivalent?
Two. PCS: Candid's population, support and transaction classification is proprietary work layered on top of the filings; we hold no equivalent and publish no crosswalk. And Premier's enrichment, profiles, demographics and impact metrics collected directly from nonprofits, does not exist in filings at any price. Candid's corpus is also larger: it states 29 million grants against our 20.1 million. Compliance screening is not on the list. /api/screening covers the same seven reference files from four agencies, decodes the IRS code books, screens OFAC across both the SDN and Consolidated lists including alias names, and returns the Rev. Proc. 2018-32 §8.01 reliance elements one by one, including the two cases where the revenue procedure says reliance does not apply at all, which a clean-looking BMF row will not tell you. A format=pdf request returns the dated one-page report for the grant file, and bulk takes 100 EINs to Candid's 25. It is on a paid key. The limit worth knowing is that a sanctions designation is effective before OFAC republishes the files we poll, so for a screen at the moment of disbursement use Treasury's live search.
Does subject mean the same thing on both APIs?
No, and this is the single most dangerous line in a migration. Candid's subject is a code from its Philanthropy Classification System; ours is the IRS NTEE taxonomy, applied to the grantee. Point a Candid client at us without converting and every subject-filtered query returns the wrong set while looking like it worked. We therefore reject a PCS-shaped value with a 400 that explains the difference, rather than failing the lookup and returning an empty page you would read as 'this funder gave nothing to education'. We accept an NTEE major-group letter (T), a full NTEE code (T31) or the cause label (Philanthropy), and we deliberately publish no PCS crosswalk: collapsing a granular PCS code into a 26-bucket NTEE group loses precision invisibly, which is the same failure in a different place.
What happens to a filter you do not support?
It is named, not dropped. Every response carries x_plinth.warnings listing each parameter we recognized and could not honor, with the reason, plus an X-Plinth-Unsupported header and an x_plinth.unsupported array of just the names. The alternative, answering 200 and quietly ignoring population=children, returns valid-looking output over a population you never asked about, which is how a migration produces substantively wrong research. Send strict=1 and the same request becomes a 400 instead; use that in your test suite.
What are the rate limits?
A free Plinth key allows 50 calls a day across /grants/* — a daily bucket rather than a monthly one, so evaluating the API doesn't cost you the month. The compliance screen, Essentials/Premier profiles and SQL are paid, and raw filing retrieval comes with the MCP connector. Paid plans raise the volume. Every response carries your remaining allowance in the X-Calls-Remaining header so you can back off before hitting the limit. Candid's developer portal documents a rate-limits topic, but we could not retrieve published numeric limits, so check with Candid directly rather than relying on a figure from us.

How this comparison was made. Every figure attributed to another company was read from that company’s own public pages on 12 August 2026, and each cell links to the page it came from. Review sites, software directories and comparison blogs were not used as sources. Where we could not confirm a capability from a vendor’s own documentation, the cell reads “not publicly documented” — that means we did not find it, not that it is absent. Pricing is the published list price and excludes discounts and custom terms. Product names are used only to identify the products compared. Tell us if something here is wrong and we will correct it.