Grantmaking.ai API
This document explains how to use the Grantmaking.ai User API. It is written for AI agents and other programmatic clients. Everything here is current as of 2026-06.
Grantmaking.ai is a shared platform to discover, evaluate, and fund high-impact AI safety work. The API gives a token holder read access to the public directory of organizations and projects, write access to the entities they are authorized to edit, and the ability to read/post comments on those entities.
Quickstart
| Setting | Value |
|---|---|
| Base URL | https://app.grantmaking.ai/api/v1 |
| Auth | Authorization: Bearer xg_... — required for writes, /me, and comment contents |
| No key? | Read-only GETs work without one (lower rate limit, private data hidden) |
| First call | GET /me — tells you what your key can do |
| Rate limit | 100 requests/minute per key; ~30/minute per IP without a key |
| Format | JSON in, JSON out. Success → { "data": ... }, error → { "error": "message" } |
Try a public read with no key:
curl "https://app.grantmaking.ai/api/v1/organizations?limit=5"
…or an authenticated call with your key:
curl "https://app.grantmaking.ai/api/v1/me" -H "Authorization: Bearer $API_KEY"
Anonymous (keyless) access
These endpoints work without any Authorization header:
GET /organizationsandGET /organizations/{id}GET /projectsandGET /projects/{id}GET /organizations/{id}/commentsandGET /projects/{id}/comments— returns only{ "data": { "commentCount": N }, "meta": { "message": "…" } }(the number of public comments), never the comment contents.
Anonymous rules:
- You are treated as a non-admin viewer: entities with private funding return
nullfor their funding fields, exactly as for non-admin tokens. - Rate limit is per IP, roughly 30 requests per 60 seconds. 429 responses
carry a
Retry-Afterheader. - Responses are CDN-cached, so anonymous data can be up to ~5 minutes stale.
- Everything else —
GET /me, all PATCH/POST/DELETE, and comment contents — requires a bearer token and returns 401 without one. Sending anAuthorizationheader with an invalid token is a 401, not a fallback to anonymous. (The one keyless write isPOST /feedback— see Feedback & bug reports below.)
If you need comment contents, write access, or the higher rate limit: sign in at https://app.grantmaking.ai and create an API key yourself on the Settings page — it's self-serve.
Critical gotchas
- Always use the
app.grantmaking.aihost. That's where the API lives; the marketing site atgrantmaking.aiis a separate host. If you send a request to a host that redirects to the canonical one (e.g. anhttp://URL or another alias), HTTP clients strip theAuthorizationheader when following a cross-host redirect. For token-only endpoints that means a confusing 401; for anonymous-enabled GETs it's worse — the request silently succeeds as anonymous, so an admin token can get a response with private fields nulled and never see an error. Send requests directly tohttps://app.grantmaking.ai. - Start with
GET /me. It tells you whether your token is admin-scoped and, if not, exactly which organizations and projects you can edit — including ready-madeapiPath/commentsApiPathvalues you can call directly. Do not guess at permissions; discover them. - PATCH bodies reject unknown fields. Sending any field not in the
allowlists below returns HTTP 400
Unsupported field: .... Send only supported fields. - List responses truncate long text.
GET /organizationsandGET /projectsclip long free-text fields to 2,000 characters (marked with a trailing…and a row-level"truncated": true). Fetch the single-entity endpoint for full text. - Respect HTTP 429. On rate limit you get a
Retry-Afterheader in seconds. Wait that long before retrying; do not retry in a tight loop.
Authentication
Read-only GETs on organizations and projects work without a key (see
Anonymous access above). For everything else,
pass a profile API key (format xg_…) as a bearer token:
Authorization: Bearer xg_...
Content-Type: application/json
Tokens are issued with a fixed scope (admin or non-admin) at creation time and do not expire unless an expiry was set at issuance.
Getting a key is self-serve. Any signed-in user can create one at
https://app.grantmaking.ai/settings (API Keys panel): name it, optionally set
an expiry, and copy the xg_… value — it is shown once at creation and
only a hash is stored. Settings-created keys are non-admin by default, even
for admin accounts; their editable set is derived from the profile's email
domain (see GET /me). Admin-scoped and reviewer-scoped keys are minted
through the admin token route by explicit opt-in. If you are an AI agent
without an account, ask the human you work for to create a key from their
Settings page.
All auth failures return HTTP 401 with { "error": "..." }:
| Situation | Message |
|---|---|
No Authorization header | Missing Authorization header |
Header isn't Bearer <token> | Invalid Authorization header format. Expected: Bearer <token> |
| Token not found | Invalid API token |
| Token expired | API token has expired |
| Token revoked | API token has been revoked |
Permission model
- Admin-scoped tokens can read and edit any organization or project.
- Non-admin tokens can edit an organization (and its projects) when the email domain on the token's profile matches the organization's website domain. Everything else is read-only.
- Reviewer-scoped tokens (
token.isReviewer: true, set per-token at mint time; admins are implicitly reviewers) can additionally call the Reviewer API to read submitted applications, score them, and leave private reviewer comments. - All valid tokens can read the public directory (
GET /organizations,GET /projects) and edit their own profile (PATCH /me/profile).
Discovery: GET /me
Always call this first. The response shape:
{
"data": {
"profile": { "id": "…", "email": "…", "name": "…" /* … */ },
"token": { "id": "…", "isAdmin": false, "isReviewer": false },
"access": {
"filtered": true, // false → admin: can edit everything
"canEditAnyOrganization": false,
"canEditAnyProject": false,
"editableOrganizations": [
// null when filtered: false
{
"id": "…",
"name": "…",
"descriptionShort": "…",
"websiteUrl": "…",
"apiPath": "/api/v1/organizations/<id>",
"commentsApiPath": "/api/v1/organizations/<id>/comments",
},
],
"editableProjects": [
/* same shape, plus orgId + organization */
],
},
"capabilities": {
/* per-area flags, e.g. commentsRequireEditableEntity */
},
},
}
access.filtered: false→ the key is admin-scoped;editableOrganizationsandeditableProjectsarenullbecause admins can edit everything.access.filtered: true→ only the listed entities are writable. Use the providedapiPath/commentsApiPathvalues directly.token.isReviewer: true(or any admin token) → the key has reviewer scope and can call the Reviewer API.data.capabilities.applicationsadvertises the matching review capability as{ "canReview": true, "apiPath": "/api/v1/applications" }. Reviewer scope is a per-token flag set at mint time, independent of the profile's account type.
Endpoint reference
All paths are relative to https://app.grantmaking.ai/api/v1.
| Method & path | Purpose | Access |
|---|---|---|
GET /me | Who am I + what can I edit | Any valid token |
PATCH /me/profile | Edit your own profile (and linked public person) | Any valid token |
GET /organizations | Paginated list of organizations | Anonymous or any valid token |
GET /organizations/{id} | Read one organization (full text) | Anonymous or any valid token |
PATCH /organizations/{id} | Edit an organization | Admin, or domain match |
GET /organizations/{id}/comments | List comments (anon: count only) | Anonymous (count) / admin, domain match |
POST /organizations/{id}/comments | Post a comment / reply | Admin, or domain match |
DELETE /organizations/{id}/comments/{cid} | Delete a comment (soft delete) | Admin, or domain-match + own |
GET /projects | Paginated list of projects | Anonymous or any valid token |
GET /projects/{id} | Read one project (full text) | Anonymous or any valid token |
PATCH /projects/{id} | Edit a project | Admin, or parent-org domain match |
GET /projects/{id}/comments | List comments (anon: count only) | Anonymous (count) / admin, domain match |
POST /projects/{id}/comments | Post a comment / reply | Admin, or parent-org domain match |
DELETE /projects/{id}/comments/{cid} | Delete a comment (soft delete) | Admin, or domain-match + own |
GET /tags | Tag vocabulary + per-entity usage counts | Anonymous or any valid token |
POST /feedback | Send feedback or a bug report | Public (no key needed) |
GET /applications | List submitted applications (reviewer view) | Reviewer or admin |
GET /applications/{id} | Read one full application | Reviewer or admin |
PUT /applications/{id}/review | Set/replace your own score (+ optional comment) | Reviewer or admin |
DELETE /applications/{id}/review | Remove your own score | Reviewer or admin |
PUT /applications/{id}/claim | Claim an application ("I'm reviewing this") | Reviewer or admin |
DELETE /applications/{id}/claim | Release your claim | Reviewer or admin |
POST /applications/{id}/comments | Post a private comment on the applicant project | Reviewer or admin |
GET /applications/{id}/comments | Read private comments on the applicant project | Reviewer or admin |
Comments support create / list / delete only (no edit), and exist only on organizations and projects (not people or funds).
The /applications endpoints are the reviewer API, gated behind reviewer or
admin token scope. They are never anonymous. See Reviewer API.
Feedback & bug reports
Found a bug, or have feedback about the API or the platform? POST /feedback
is the one write that works without a key — built so an AI agent (or a
person) can fire off a quick note or bug ticket with zero setup. We read these.
Request body (JSON):
| Field | Required | Notes |
|---|---|---|
message | yes | The feedback or bug report. Max 8,000 characters. |
email | no | A contact address, if you'd like a reply. |
source | no | Where this came from, e.g. a URL or "API". Max 500 characters. |
- No
Authorizationheader required. Sending a valid token simply attributes the feedback to your account; it is never required. - Rate limited. A few submissions per minute per IP. Separately, the number
of notification emails we send is globally capped per hour — so during a
flood your submission is still stored even when it doesn't trigger an
email.
429responses carry aRetry-Afterheader (seconds). - Unknown fields are rejected with
400 Unsupported field: ....
curl -X POST "https://app.grantmaking.ai/api/v1/feedback" \
-H "Content-Type: application/json" \
-d '{"message":"GET /projects 500s when limit=0","email":"you@example.com","source":"API"}'
Success → 201 { "data": { "id": "<uuid>" } }.
Response and error conventions
Success responses wrap the payload in data:
// Lists (truncated text fields):
{ "data": [ /* rows, each with "truncated": bool */ ], "meta": { "total": 142, "limit": 50, "offset": 0 } }
// Single entity (full text, no "truncated" flag):
{ "data": { /* object */ } }
// PATCH:
{ "data": { "id": "…", "updated": true } }
// POST .../comments → HTTP 201:
{ "data": { "id": "…", "content": "…", "visibility": "public", /* … */ } }
// DELETE .../comments/{cid}:
{ "data": { "id": "…", "deleted": true, "mode": "soft-deleted" } }
Errors are always { "error": "message" } with an appropriate status:
| Status | Meaning |
|---|---|
| 400 | Malformed JSON, invalid UUID, unknown field, or failed validation |
| 401 | Auth failure (see table above) |
| 403 | Valid token, but not authorized for this entity or field |
| 404 | Entity/comment not found, hidden, or not visible to you |
| 409 | Profile patch needs a linked public person that doesn't exist |
| 429 | Rate limited — honor the Retry-After header (seconds) |
| 500 | Server error — safe to retry once after a short delay |
Token-authenticated responses carry Cache-Control: private, no-store; do
not cache them. Anonymous 200s carry
Cache-Control: public, s-maxage=60, stale-while-revalidate=240 (plus
Vary: Authorization) and may be served from the CDN, up to ~5 minutes stale.
Rate limiting
- With a token: 100 requests per rolling 60 seconds, per token.
- Anonymous (no token): roughly 30 requests per 60 seconds, per IP, enforced at the edge.
Exceeding either returns HTTP 429 { "error": "Too many requests" } with a
Retry-After header in whole seconds.
Agent guidance:
- Use pagination (
limit=100) instead of many small requests. - On 429, sleep for
Retry-Afterseconds, then resume. - For bulk reads of the full directory, ~6 requests fetch all organizations
and ~13 fetch all projects at
limit=100— comfortably inside the limit, even anonymously. - The anonymous budget is shared across all your keyless requests.
CDN-cache hits (repeat list/entity GETs) may be served at the edge without
consuming it, but non-cached endpoints like
.../commentsalways count — a mixed workload can hit 429 sooner than the headline number suggests.
Reading the directory
Both list endpoints share a common set of search, filter, and sort params
(below). limit/offset are clamped; every other listed param returns 400
on an invalid value (unknown enum, malformed date/UUID, unsupported sort
column). Unknown query keys (e.g. utm_source) are ignored. meta.total
reflects the filtered count. Filters compose with AND.
Shared params (organizations + projects):
| Param | Type | Behavior |
|---|---|---|
limit | 1–100 (default 50) | Clamped, not rejected (limit=0 → 1, limit=500 → 100). |
offset | ≥ 0 (default 0) | Clamped. |
q | string (≤ 100 chars) | Case-insensitive substring match on name only (%/_/\ are matched literally). |
location | string (≤ 100 chars) | Case-insensitive substring match on the location field. |
descriptionContains | string (≤ 100 chars) | Case-insensitive substring match across the description fields (OR). Orgs: descriptionShort/descriptionMedium/descriptionFull; projects add theoryOfChange. Blank → ignored. |
tags | slug (multi, ≤ 25) | Filter to entities carrying the given tag slugs (see GET /tags). Combine with tagMatch. Present-but-empty (tags=) → 400. Unknown slugs are not an error (see tagMatch). |
tagMatch | any | all | Default any. any = entity has at least one of the slugs; all = entity has every slug. Other value → 400. |
isActivelyFundraising | true/false/1/0 | Exact match. |
updatedAfter | ISO datetime or YYYY-MM-DD | updatedAt >= value. The main incremental-sync filter. |
updatedBefore | ISO datetime or YYYY-MM-DD | updatedAt < value; a date-only value means the next UTC midnight (end-exclusive). |
sort | name | createdAt | updatedAt | Default name. Any other value → 400. id is always appended as a stable tiebreaker. |
order | asc | desc | Default asc. |
Multi-value params (orgType, status, orgIds, tags) accept either
repeated keys (?status=active&status=paused) or comma-separated values
(?status=active,paused).
Unknown tag slugs (not in the vocabulary returned by GET /tags) are never
a 400 — they simply narrow results: ignored under tagMatch=any, and they make
tagMatch=all yield an empty set (an entity cannot have a slug that does not
exist). Example: ?tags=mech-interp,governance&tagMatch=all returns only
entities tagged with both mech-interp and governance.
No funding filters or sort. There is intentionally no way to filter or sort on
annualBudget,fundingGoal,fundingRaisedToDate, or any other funding amount/JSON field. Result inclusion/order on a redacted private amount would leak it. Private-funding rows still appear in results when they match a public filter — their funding fields just come backnull.
GET /organizations
Returns non-hidden organizations. Supports all shared params above, plus:
| Param | Type | Behavior |
|---|---|---|
orgType | enum (multi) | IN (...) over org_type (nonprofit, research_org, academic, …). Unknown value → 400. |
Responses also include read-only fields not listed in the PATCH tables —
e.g. logoUrl, createdAt, updatedAt (and squareLogoUrl on projects).
Treat any field you don't recognize as read-only.
Long text fields (descriptionShort, descriptionMedium, descriptionFull,
theoryOfChange) are truncated to 2,000 characters in list responses; rows
with clipped fields carry "truncated": true. GET /organizations/{id}
returns full text (404 if not found or hidden).
Funding privacy: when an org has isFundingPrivate: true, the fields
annualBudget, monthlyBurnRate, currentRunwayMonths, fundingGoal, and
fundingRaisedToDate come back as null to non-admin tokens. A null there
does not mean the data doesn't exist — do not "fix" it by writing values.
GET /projects
Same pagination, truncation, and shared filter/sort params as organizations, plus:
| Param | Type | Behavior |
|---|---|---|
orgId | UUID | Filter to one organization's projects. Malformed → 400 Invalid orgId format. |
orgIds | UUID (multi) | IN (...) over org_id; up to 100 ids. Mutually exclusive with orgId — sending both → 400. |
status | enum (multi) | IN (...) over status (active, completed, paused, cancelled, upcoming). Unknown → 400. |
Project rows carry orgId only — there is no embedded organization
object in GET /projects or GET /projects/{id}. The embedded
organization (with name, apiPath, etc.) appears only inside /me's
editableProjects. To resolve an org, fetch GET /organizations/{orgId}.
Funding privacy: when a project has isFundingPrivate: true, non-admin
tokens see fundingGoals, fundingAmountRequested, fundingRaisedToDate,
annualBudget, monthlyBurnRate, and currentRunwayMonths as null.
GET /tags
Returns the full controlled tag vocabulary with usage counts per entity type, so
you know which tags slugs to pass to GET /organizations?tags=… or
GET /projects?tags=…. Anonymous-readable and CDN-cacheable. Ordered by
label ascending, then slug.
Each tag also carries a kind (type | work | area) describing which
facet of the taxonomy it belongs to, and a meta boolean flagging broad
cross-cutting area tags. Both are informational — the tags filter keys off
slug regardless of kind/meta.
| Param | Type | Behavior |
|---|---|---|
entityType | organization | project | person | fund | Optional. Returns only tags used by that entity type (count > 0). Invalid value → 400. |
curl "https://app.grantmaking.ai/api/v1/tags"
{
"data": [
{
"slug": "mech-interp",
"label": "Interp",
"description": "…",
"kind": "area",
"meta": false,
"counts": { "organizations": 12, "projects": 30, "persons": 5, "funds": 1, "total": 48 }
}
],
"meta": { "total": 1 }
}
Editing entities
curl -X PATCH "https://app.grantmaking.ai/api/v1/organizations/$ORG_ID" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"descriptionShort":"Updated short description"}'
Rules that apply to all PATCH endpoints:
- Only allowlisted fields may be sent; any other key → 400
(
Unsupported field: …). - A body with zero recognized fields → 400
(
At least one supported field is required). name, when present, must be a non-empty string.- Setting a nullable field to
nullclears it. Omitting a field leaves it unchanged. Send only the fields you intend to change. - Domain-authorized non-admin keys may PATCH funding amount fields on entities
they can edit even when
isFundingPrivate: trueredacts those same fields from their GET responses. This is intentional so owners can maintain private funding data through integrations. Because PATCH returns only{ "id": "...", "updated": true }, read the current policy first and avoid sending funding fields unless you intend to replace or clear them.
Organization fields (PATCH /organizations/{id})
| Field | Type / rules |
|---|---|
name | string, required if present |
descriptionShort, descriptionMedium, descriptionFull, theoryOfChange | string or null |
websiteUrl, linkedinUrl | valid http/https URL or null |
location, fundingStage, fiscalSponsor, trackRecord | string or null |
orgType | known org type string, or null to clear |
foundedDate | YYYY-MM-DD or null |
teamSize, currentRunwayMonths | integer ≥ 0 or null |
annualBudget, monthlyBurnRate, fundingGoal, fundingRaisedToDate | decimal ≥ 0 or null |
isActivelyFundraising, isFundingPrivate | boolean |
donationLinks | array of { platform, url } (url must be valid http/https), or null |
Non-admin keys cannot set
websiteUrl→ HTTP 403. The website domain determines who can edit the org, so changing it would change permissions.
Project fields (PATCH /projects/{id})
| Field | Type / rules |
|---|---|
name | string, required if present |
descriptionShort, descriptionMedium, descriptionFull, theoryOfChange, expectedDuration | string or null |
websiteUrl, linkedinUrl | valid http/https URL or null |
location, fundingStage, fiscalSponsor, trackRecord | string or null |
status | one of active, completed, paused, cancelled, upcoming |
startDate, endDate | YYYY-MM-DD or null (endDate cannot precede startDate) |
teamSize, currentRunwayMonths | integer ≥ 0 or null |
annualBudget, monthlyBurnRate, fundingRaisedToDate | decimal ≥ 0 or null |
isActivelyFundraising, isFundingPrivate | boolean |
fundingGoals | object { minimum?, goal?, stretch? }, each a number ≥ 0, or null |
orgId | UUID or null |
donationLinks | array of { platform, url }, or null |
Non-admin keys cannot set
orgId→ HTTP 403. Edit rights on a project derive from its parent organization, so reassigning it is admin-only.
Profile fields (PATCH /me/profile)
Writes to two records: your account profile and, where applicable, your linked public person.
| Field | Written to | Notes |
|---|---|---|
name | profile + person | required if present |
bio | profile + person | |
displayName | profile | auto-filled from name if you set name without displayName |
titleAndOrg | profile | max 120 characters |
personalWebsiteUrl, linkedinUrl, twitterUrl | person | valid http/https URL or null |
lesswrongHandle, eaForumHandle, location | person | string or null |
Person-only fields (everything except
name/bio/displayName/titleAndOrg) require a linked public profile; without one they return HTTP 409 (No linked public profile found).
Comments
Anonymous callers may GET .../comments but receive only the number of
public comments:
{
"data": { "commentCount": 4 },
"meta": { "message": "Comment contents are not available without an API key. …" },
}
Reading comment contents and posting comments requires being an authorized editor of the entity (admin token, or matching email domain). Other valid tokens get 403.
What you see in GET .../comments depends on the token:
- Admin / reviewer / funder tokens see all public + all restricted comments.
- Other authorized editors see all public comments plus only the restricted comments they themselves authored.
Posting
curl -X POST "https://app.grantmaking.ai/api/v1/organizations/$ORG_ID/comments" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"Comment text","parentCommentId":null,"visibility":"public"}'
content— required, max 10,000 characters.parentCommentId— a parent comment's UUID to reply;null/omit for a top-level comment.visibility—"public"(default) or"restricted"(private note visible per the rules above).- Replies inherit restriction: replying to a
restrictedcomment forces the reply torestrictedregardless of what you send. The response echoes the storedvisibility— check it.
@-mentions
Comment content may @-mention users with the markdown form
[@Label](mention://<profileId>) where <profileId> is the user's profile
UUID. The server rewrites each mention's label to the profile's canonical
display name (so the label you send is only a hint), and downgrades to plain
text any mention of a profile that doesn't exist or has opted out of mentions
(allow_mentions = false) — the 201 response's content is the stored,
rewritten form. Mentioned users who can read the comment get an email
notification (subject to their preferences). Mentions of users who cannot
read a restricted comment are recorded but not emailed, and are returned in
the 201 response as mentionWarnings: [{ profileId, label }] (an empty array
when everyone was notified). At most 10 distinct mentions per comment.
Deleting
DELETE .../comments/{commentId}. Non-admins may delete only their own
comments. Any comment you can't act on (missing, already deleted, different
entity, not yours) returns 404 Comment not found — the endpoint never
reveals comments you can't act on.
Deletes are always soft: the row remains as a tombstone
(content: "[deleted]", author masked, deletedAt set) so threads don't
break.
Reviewer API
If your token has reviewer scope, you can do grant-review work programmatically: list submitted applications, read each one in full, record your own score, claim an application you're working on, and leave private reviewer comments. These endpoints mirror the internal review tool.
Scope check first. Every /applications endpoint requires reviewer or admin
scope. Confirm with GET /me: token.isReviewer must be true (admin tokens
qualify automatically). A valid token without reviewer scope gets HTTP 403.
Reviewer scope is a per-token flag (is_reviewer) set by an admin at mint time —
it does not follow the profile's account type, so you can't infer it; read it
from /me. There is no anonymous access to any of these endpoints, and all
responses are Cache-Control: private, no-store (never CDN-cached).
Getting a reviewer key is not self-serve: an admin mints it via
POST /api/v1/tokens with { "isReviewer": true, "profileId": "<your-profile>" },
binding the token to your own profile so your scores and comments attribute to
you. Cross-profile binding is only accepted for non-admin reviewer tokens; normal
user/admin tokens stay bound to the minting admin's own profile. Ask an admin to
issue one.
Scoring rubric
Scores are single letter grades, best to worst: S, A, B, C, F
(S is strongest). You record one score per application. The API does
no aggregation — it returns every reviewer's individual score and name, and
you can set or clear only your own. Review is not blind between reviewers: you
see all reviewers' scores and names, matching the internal UI.
GET /applications — list submitted applications
Paginated list of submitted (non-draft) applications.
| Param | Type | Behavior |
|---|---|---|
limit | 1–100 (default 50) | Clamped, not rejected. |
offset | ≥ 0 (default 0) | Clamped. |
roundId | UUID | Filter to one funding round. Malformed → 400. |
status | string (optional) | Filter by review status. |
sort | string (optional) | Order results (default newest first). |
Free-text narrative is truncated in the list ("truncated": true when
clipped); fetch the single-application endpoint for full text. Each row:
{
"data": [
{
"id": "11111111-1111-1111-1111-111111111111",
"status": "pending_review",
"dateApplied": "2026-06-18",
"createdAt": "2026-06-18T14:02:00.000Z",
"amountRequested": 75000,
"idealAmount": 120000,
"roundId": "33333333-3333-3333-3333-333333333333",
"roundName": "2026 Q3 AI Safety Round",
"applicantProjectId": "22222222-2222-2222-2222-222222222222",
"applicantName": "Interpretability Probes",
"applicantApiPath": "/api/v1/projects/22222222-…",
"apiPath": "/api/v1/applications/11111111-…",
"commentsApiPath": "/api/v1/applications/11111111-…/comments",
"tags": ["mech-interp", "evals"],
"submittedByName": "Alice Applicant",
"privateInfoIsPrivate": true,
"priorApplicationIsPrivate": false,
"reviews": [
{
"reviewerId": "44444444-…",
"reviewerName": "Alice Reviewer",
"score": "A",
"comment": "Strong team.",
"reviewedAt": "2026-06-20T10:00:00.000Z",
},
],
"claims": [
{
"reviewerId": "44444444-…",
"reviewerName": "Alice Reviewer",
"claimedAt": "2026-06-19T09:00:00.000Z",
},
],
"description": "We propose to … (clipped) …",
"fundingHistorySummary": "Prior support from … (clipped) …",
"privateInfo": "Reviewer-only context … (clipped) …",
"priorApplication": "Related ask in 2026 Q1 … (clipped) …",
"truncated": true,
},
],
"meta": { "limit": 50, "offset": 0, "count": 37 },
}
Because the whole surface is reviewer/admin-gated, the list carries every
narrative field — including the private ones (privateInfo, priorApplication) —
each truncated, with one truncated flag set when any was clipped. The
privateInfoIsPrivate / priorApplicationIsPrivate booleans report whether the
applicant marked those fields private. reviewerName may be null if a name
can't be resolved. Fetch the single application for the untruncated text.
meta.count is the number of rows returned on this page (there is no total).
curl "https://app.grantmaking.ai/api/v1/applications?roundId=$ROUND_ID&limit=50" \
-H "Authorization: Bearer $API_KEY"
GET /applications/{id} — read one application
Returns the application in full: untruncated narrative, the reviewer-only private
fields (privateInfo, priorApplication), all reviewers' scores with names, and
claims. 404 if not found or still a draft.
{
"data": {
"id": "11111111-…",
"status": "pending_review",
"dateApplied": "2026-06-18",
"createdAt": "2026-06-18T14:02:00.000Z",
"amountRequested": 75000,
"idealAmount": 120000,
"roundId": "33333333-…",
"roundName": "2026 Q3 AI Safety Round",
"applicantProjectId": "22222222-…",
"applicantName": "Interpretability Probes",
"applicantApiPath": "/api/v1/projects/22222222-…",
"apiPath": "/api/v1/applications/11111111-…",
"commentsApiPath": "/api/v1/applications/11111111-…/comments",
"tags": ["mech-interp", "evals"],
"submittedByName": "Alice Applicant",
"privateInfoIsPrivate": true,
"priorApplicationIsPrivate": false,
"reviews": [
{
"reviewerId": "44444444-…",
"reviewerName": "Alice Reviewer",
"score": "A",
"comment": "Strong team.",
"reviewedAt": "2026-06-20T10:00:00.000Z",
},
{
"reviewerId": "55555555-…",
"reviewerName": "Bob Reviewer",
"score": "B",
"comment": "",
"reviewedAt": "2026-06-21T08:30:00.000Z",
},
],
"claims": [
{
"reviewerId": "44444444-…",
"reviewerName": "Alice Reviewer",
"claimedAt": "2026-06-19T09:00:00.000Z",
},
],
"description": "Full narrative …",
"fundingHistorySummary": "Prior support from …",
"privateInfo": "Reviewer-only context.",
"priorApplication": "Related ask in 2026 Q1 …",
},
}
PUT /applications/{id}/review — score it
Sets or replaces your own score (keyed to your token's bound profile).
Idempotent — re-PUT to change your grade.
curl -X PUT "https://app.grantmaking.ai/api/v1/applications/$APP_ID/review" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"score":"A","comment":"Strong team, clear methodology."}'
score— required, one ofS/A/B/C/F(other value → 400).comment— optional, ≤ 10,000 chars; omitted → empty string.
You can only write your own score. Success →
{ "data": { "id": "<applicationId>", "score": "A", "reviewedAt": "<ISO timestamp>" } }
(the reviewedAt the server stamped on the saved review).
DELETE /applications/{id}/review removes your own score (no body) →
{ "data": { "id": "<applicationId>", "removed": true } }.
PUT / DELETE /applications/{id}/claim — claim / release
The claim marks "I'm reviewing this". PUT sets your claim, DELETE clears it;
both act only on your own profile and take no body.
curl -X PUT "https://app.grantmaking.ai/api/v1/applications/$APP_ID/claim" \
-H "Authorization: Bearer $API_KEY"
Success → { "data": { "id": "<applicationId>", "claimed": true, "claimedAt": "<ISO timestamp>" } }
for PUT (or { "data": { "id": "<applicationId>", "claimed": false } } for
DELETE — no claimedAt).
POST / GET /applications/{id}/comments — private notes
Leave and read private (restricted) reviewer notes on the application's applicant project. You pass the application id; the endpoint resolves the underlying project internally, only for a submitted (non-draft) application — which is also the safety guard (a draft or unknown id → 404, never another project's comments).
These comments are restricted: visible to reviewers, funders, and admins,
and redacted for everyone else (the same redaction model as the
Comments section — redacted rows arrive with isRedacted: true and
masked content). Use them for reviewer-internal notes on the applicant. The
comment is attributed to your token's bound profile.
# Post a private note on the applicant project behind this application
curl -X POST "https://app.grantmaking.ai/api/v1/applications/$APP_ID/comments" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"Reference-checked the PI; checks out."}'
# Read the private comment thread
curl "https://app.grantmaking.ai/api/v1/applications/$APP_ID/comments" \
-H "Authorization: Bearer $API_KEY"
content— required, ≤ 10,000 chars.parentCommentId— optional UUID to reply within the thread.
Response shapes match the org/project comment endpoints (POST → 201,
GET → { "data": [ … ] }).
Reviewer privacy
The /applications endpoints are strictly reviewer/admin-gated. Private
application fields (privateInfo, priorApplication) and full narratives never
reach a non-reviewer token — it gets 403 before any application data is read.
Applicant contact details and internal identifiers (e.g. the submitter's user
id) are not returned — the only submitter field exposed is submittedByName, a
display name.
Recommended agent workflow
- No API key? You can still do step 2 (read the directory) anonymously at
the lower rate limit. Skip
/me, writes, and comment contents. GET /me— learn your scope and editable entities.- To survey the ecosystem: page through
GET /organizationsandGET /projectswithlimit=100, then fetch single entities for any rows markedtruncated: truethat you need in full. - To update data: confirm the entity is in your editable set (or that you're
admin), PATCH only the changed fields, and verify the
{ "updated": true }response. - To leave notes for humans: POST a comment; use
"restricted"for internal/reviewer-only notes and"public"otherwise. - Back off on any 429 using
Retry-After; treat 5xx as retryable once.
Windows / PowerShell note
On Windows PowerShell, curl aliases Invoke-WebRequest, which doesn't
accept -X/-H/-d and mangles UTF-8 bodies. Use the real binary
curl.exe and pass JSON bodies from a UTF-8 file:
curl.exe -X POST "https://app.grantmaking.ai/api/v1/organizations/$ORG_ID/comments" `
-H "Authorization: Bearer $API_KEY" `
-H "Content-Type: application/json; charset=utf-8" `
--data-binary "@body.json"