Plinth US Grants Data Credentials API

Getting a key, including the one an agent can mint for itself.

GitHub OpenAPI

Operations 5

0 of 5 operations are conformant: apis.io called them with no credentials and the responses matched this contract; 2 more answered but could not be checked against a schema. Checked 2026-11-12. how this works · status file

GET /api/v1/agent-key How to mint a trial key by signing a request observed #
POST /api/v1/agent-key Mint a trial API key — no account, no host, no signature #
POST /api/v1/agent-key/challenge Take another challenge on a key you hold #
GET /api/v1/agent-key/challenge How the challenge loop works observed #
POST /api/v1/agent-key/answer Answer a challenge and unlock the key #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/plinth-us-grants-data:plinth-us-grants-data-credentials-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

plinth-us-grants-data-credentials-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Plinth Grants Credentials API
  summary: US foundation and nonprofit funding data from public IRS Form 990 and 990-PF filings.
  description: 'A read API over the US grantmaking universe, read from public IRS Form 990, 990-EZ and 990-PF e-file filings.


    The funder-grantee graph traverses in BOTH directions: filter by `funder_id` for everything a foundation funded, or by `recip_id` for every funder behind a nonprofit.


    Start with `/api/v1/search` to turn an organization name into an EIN and a canonical page URL — it needs no key. The grant endpoints need a free key (50 calls a day); the organization endpoints (/compliance, /essentials, /premier) need a paid one.


    **No CORS headers, deliberately.** This is a server-side API: a key in browser JavaScript is a key you have published, and every allowance here is per-key. Call it from your backend, or from an agent. There is no browser-callable subset planned.


    Figures are read directly from named IRS filings. The IRS releases e-file data on a 12-24 month lag, so every figure is dated to its fiscal year rather than to today. Funding relationships are reported as association, never as causation. Organizations that do not e-file may be absent.'
  version: 1.0.0
  contact:
    name: Plinth
    url: https://data.useplinth.com/developers
  license:
    name: 'Source data: IRS public domain'
    url: https://www.irs.gov/charities-non-profits/tax-exempt-organization-search-bulk-data-downloads
  termsOfService: https://data.useplinth.com/terms
servers:
- url: https://data.useplinth.com
  description: Plinth US philanthropy data
security:
- ApiKeyAuth: []
tags:
- name: Credentials
  description: Getting a key, including the one an agent can mint for itself.
paths:
  /api/v1/agent-key:
    get:
      tags:
      - Credentials
      operationId: describeAgentKey
      security: []
      summary: How to mint a trial key by signing a request
      description: Returns the signing requirements, the tier ladder and a worked example request. No key, no account, no signature needed to read this.
      responses:
        '200':
          description: 'How to get a key: the keyless challenge route, the signing alternative, and the tier ladder.'
          content:
            application/json:
              schema:
                type: object
                description: Human- and machine-readable instructions. `what_this_is` leads with the route that needs no credential; `signing_instead` is the higher-tier alternative; `tiers` is the full ladder.
    post:
      tags:
      - Credentials
      operationId: mintAgentKey
      security: []
      x-plinth-auth: none (challenge) | rfc9421-web-bot-auth (higher tiers)
      summary: Mint a trial API key — no account, no host, no signature
      description: 'Mints a short-lived, read-only evaluation key. No account, no card, no browser, and nothing created in anyone else''s name.


        **The ordinary route needs no credential of any kind.** POST here with `purpose` and `source` and you get a key immediately — but LOCKED: it may make only the calls its challenge names. Make them, read the fields the questions ask about, POST those values to `/api/v1/agent-key/answer`, and the key unlocks for general use. When that budget is spent, take another challenge. As often as you like, free.


        Every fiscal year in the corpus is readable; what is small is the row cap and the per-challenge budget, both announced in `x_plinth.warnings` at `caveat` severity.


        **Signing is the alternative, not the requirement.** A caller that can publish an Ed25519 key directory and sign per RFC 9421 (Web Bot Auth profile: cover `@authority` and `signature-agent`, tag `web-bot-auth`, with `created`, `expires`, a `nonce` and a `keyid` that is the base64url JWK SHA-256 thumbprint) is issued at a higher tier with a larger budget and no challenges. That allowance IS a total and never refills. GET this path for the full ladder.'
      parameters:
      - name: Signature-Agent
        in: header
        required: false
        schema:
          type: string
        description: 'The https origin publishing your key directory, as a structured-field string: "https://your-host.example".'
      - name: Signature-Input
        in: header
        required: false
        schema:
          type: string
        description: RFC 9421 covered components and parameters, e.g. sig=("@authority" "signature-agent");created=…;expires=…;keyid="…";alg="ed25519";nonce="…";tag="web-bot-auth"
      - name: Signature
        in: header
        required: false
        schema:
          type: string
        description: 'sig=:<base64 ed25519 signature>:  Send all three signature headers or none.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentKeyRequest'
      responses:
        '201':
          description: A trial key, its expiry, its budget, and the challenge it arrives locked to.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrialKey'
        '400':
          description: '`purpose` or `source` missing or too short.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: A signature was presented and did not verify, or was replayed. The body says which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: This signature-agent host has minted its allowance of trial keys today.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '502':
          description: Your key directory could not be read — your signature may be fine.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '503':
          description: Trial keys are at capacity; get a free key with an account instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/agent-key/challenge:
    post:
      tags:
      - Credentials
      operationId: takeChallenge
      security: []
      x-plinth-auth: trial-key
      summary: Take another challenge on a key you hold
      description: A trial key whose grant is spent asks for three new calls, is locked to them, answers, and is unlocked again — as often as needed. Replacing an outstanding challenge is allowed; it grants extra calls only if the previous questions turned out to be unanswerable from our side too.
      responses:
        '200':
          description: Three calls this key may now make, and where to answer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChallengeIssued'
        '401':
          description: No key, or not a live trial key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    get:
      tags:
      - Credentials
      operationId: describeChallenge
      security: []
      summary: How the challenge loop works
      description: Readable without a key, so an agent that arrives here learns the shape rather than meeting a 401.
      responses:
        '200':
          description: The loop, in order.
          content:
            application/json:
              schema:
                type: object
                description: How the mint / answer / re-challenge cycle fits together.
  /api/v1/agent-key/answer:
    post:
      tags:
      - Credentials
      operationId: answerChallenge
      security: []
      x-plinth-auth: trial-key
      summary: Answer a challenge and unlock the key
      description: Send the values of the fields the questions named. We re-read those calls to check, so nothing is stored at issue time and nothing can drift from the data it described.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnswerRequest'
      responses:
        '200':
          description: Unlocked, with the general call budget added.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnlockResult'
        '400':
          description: 'Wrong answers, or the wrong number of them. Which one was wrong is deliberately not said. The last attempt on a challenge REPLACES it: `error: challenge_replaced`, with fresh questions in the same body.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: No key, or not a live trial key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '409':
          description: That key has no challenge outstanding.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Too soon after a wrong answer. `Retry-After` and `retry_after_seconds` say how long; the attempt was neither counted nor checked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    TrialKey:
      type: object
      properties:
        code:
          type: integer
        message:
          type: string
        key:
          type: string
          description: The credential. Send as X-API-Key.
        key_prefix:
          type: string
        plan:
          type: string
        tier:
          type: string
        tier_label:
          type: string
        expires_at:
          type: string
          format: date-time
        calls_ceiling:
          type: integer
        calls_used:
          type: integer
        calls_remaining:
          type: integer
        budget_is:
          type: string
        max_limit:
          type: integer
        max_page:
          type:
          - integer
          - 'null'
        scope:
          type: string
        challenge:
          type: object
          description: Present when the key arrives LOCKED. Until these are answered the key may make only the calls named here.
          properties:
            questions:
              type: array
              items:
                $ref: '#/components/schemas/ChallengeQuestion'
            answer_at:
              type: string
            attempts:
              type: string
              description: How many answers this challenge takes and how they are spaced. Running out draws a FRESH challenge rather than locking the key.
        terms:
          type: object
          description: What is enforced, and what we ask of you.
      required:
      - code
      - key
      - tier
      - expires_at
    AgentKeyRequest:
      type: object
      required:
      - purpose
      - source
      properties:
        purpose:
          type: string
          minLength: 10
          description: What you are evaluating. Recorded, never verified, never gates — but required.
          examples:
          - Evaluating grant-graph coverage for a funder-research tool
        source:
          type: string
          description: 'Who or what is calling: an agent name, product or host. Recorded, unverified, required.'
          examples:
          - acme-research-agent/1.0
        contact:
          type: string
          description: Optional. A URL or address we can reach you at.
    AnswerRequest:
      type: object
      required:
      - answers
      properties:
        answers:
          type: array
          items:
            type: string
          description: One value per question, in the order given. Compared case-insensitively; "yes"/"true" and "$1,234"/"1234" are equivalent.
    ApiError:
      type: object
      description: Every failure answers with this envelope; `message` is safe to show a user.
      properties:
        code:
          type: integer
        message:
          type: string
      required:
      - code
      - message
    ChallengeIssued:
      type: object
      properties:
        code:
          type: integer
        message:
          type: string
        questions:
          type: array
          items:
            $ref: '#/components/schemas/ChallengeQuestion'
        answer_at:
          type: string
        attempts:
          type: string
          description: How many answers this challenge takes and how they are spaced. Running out draws a FRESH challenge rather than locking the key.
      required:
      - code
      - questions
      - answer_at
    ChallengeQuestion:
      type: object
      properties:
        call:
          type: string
          description: The exact request to make, method and URL.
        ask:
          type: string
          description: What to read out of that response, in plain English.
      required:
      - call
      - ask
    UnlockResult:
      type: object
      properties:
        code:
          type: integer
        message:
          type: string
        calls_remaining:
          type: integer
        expires_at:
          type: string
          format: date-time
        when_spent:
          type: object
        terms:
          type: object
      required:
      - code
      - calls_remaining
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Free key includes 50 calls a day across /grants/*; the organization endpoints and SQL need a paid plan. Get one at https://data.useplinth.com/pricing.
externalDocs:
  description: Developer documentation
  url: https://data.useplinth.com/developers