GrantTroveGrantTrove Docs
Doing the work

Reading the catalogue from your own code

Creating an API key, making your first request, and the one convention that will break a client written on habit.

The API is a read-only JSON copy of the catalogue: records, grantmakers, recipients, and a feed of what has changed. It is part of Studio.

The data is not. Every field the API returns is one the site publishes for free on the corresponding page, source link and check date included — the API is a convenience for machines, not a paywall around facts.

Creating a key

Go to Settings → API keys and choose Create API Key. Give it a name that says where it will run — nightly-mirror, dashboard-staging — because that name is the only thing you will have to go on when you decide which of three keys is safe to revoke.

The key is shown once. Only a hash of it is stored, which means nobody here can read it back to you either. Copy it into wherever it belongs before closing the dialog.

A Studio account may hold three keys at a time. Three, so that each environment can carry its own and any one of them can be rotated without a gap. When the table is full the Create button is disabled and the line beside it says so; revoke one to make room.

Your first request

Send the key as a bearer token:

curl -H "Authorization: Bearer $GRANTTROVE_KEY" \
  "https://granttrove.com/api/v1/grants?state=CA&limit=5"

The filter names are the ones the catalogue page uses, and the rows come back in the same order that page shows them. A URL you are looking at in the browser answers as an API call with /api/v1 in front of it.

Every response has the same three keys:

{
  "data": [ ... ],
  "next_cursor": "eyJvIjoyNSwidCI6MTc1...",
  "as_of": "2026-09-07T12:35:00.000Z"
}

as_of is when we read the database. It is not the same fact as last_verified_at, which travels on each record and says when we last read the funder's own page. Keep both.

The one thing to get right

Null means the source did not say. It never means zero, never means an empty list, and never means false.

  • amount_max: null means the source stated no ceiling. A zero would be a number you would go on to average.
  • match_required: null means the source said neither way. Reading it as false tells an applicant no match is needed, and that can cost them a filing.
  • elig_states: null means no stated geographic restriction. An empty array would read as "no state qualifies", which is the opposite claim.

Two money figures never share a label either. amount_max is what one applicant may receive; amount_total_pool is what the whole programme has to give away. On a grantmaker, giving_total is unusable without giving_data_year, and the two always travel together.

Working through a long list

Follow next_cursor until it comes back null. Stop on null rather than on a short page — a short page is not the end.

cursor=""
while :; do
  page=$(curl -sG -H "Authorization: Bearer $GRANTTROVE_KEY" \
    --data-urlencode "limit=100" \
    ${cursor:+--data-urlencode "cursor=$cursor"} \
    "https://granttrove.com/api/v1/grants")
  echo "$page" | jq -c '.data[]'
  cursor=$(echo "$page" | jq -r '.next_cursor // empty')
  [ -z "$cursor" ] && break
done

The cursor pins the clock as well as the position. The catalogue's order depends on the time — a record drops down the list the moment its own deadline passes — so every page of one walk is computed against the instant the first page was. That instant is the as_of on every page of it.

Keeping a copy in step

Once you hold a copy, /api/v1/changes is how you keep it current:

curl -H "Authorization: Bearer $GRANTTROVE_KEY" \
  "https://granttrove.com/api/v1/changes?since=2026-09-01"

It carries records added, records the sweep closed, and deadline, amount and title moves — the same data the change log publishes.

observed_at is when we saw the change. A funder can move a deadline in March and we can re-read the page in April; no source publishes the first date, so the field is named for the one we actually have. Do not present it to your own readers as the date the change was made.

Ask for a window and you get all of it or a refusal. A window holding more changes than one walk will serve comes back as window_too_large rather than as a silent prefix: rows you never hear about look exactly like rows that never changed, and that is the one error a mirror cannot detect for itself.

Being polite to the API

Each key may make 5,000 requests a day. The remainder comes back on every response in X-RateLimit-Remaining.

Responses carry an ETag. Send it back as If-None-Match and an unchanged resource answers 304 with no body, which is most of what a polling client costs. Responses may be cached for five minutes.

When something is refused

Every refusal is JSON with a stable error slug. Branch on the slug; the sentence beside it is for your log.

401 and 403 are different answers and it is worth handling them apart. A 401 means the credential is not valid — wrong string, or revoked. A 403 means the credential is real and the account's plan does not include the API, which is what a key outliving a downgrade looks like. The plan is read on every request and is never baked into the key.

The full reference

The developer page lists every endpoint and every parameter, and openapi.json describes them in a form a code generator can read. That document needs no key.

Table of Contents