grantmaking.ai
Actively FundraisingRecent ActivityFull Database
Resources
grantmaking.ai
Actively FundraisingRecent ActivityFull Database
Resources
Apply to our funding roundApply for funding
API

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

SettingValue
Base URLhttps://app.grantmaking.ai/api/v1
AuthAuthorization: Bearer xg_... — required for writes, /me, and comment contents
No key?Read-only GETs work without one (lower rate limit, private data hidden)
First callGET /me — tells you what your key can do
Rate limit100 requests/minute per key; ~30/minute per IP without a key
FormatJSON 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 /organizations and GET /organizations/{id}
  • GET /projects and GET /projects/{id}
  • GET /organizations/{id}/comments and GET /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 null for 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-After header.
  • 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 an Authorization header with an invalid token is a 401, not a fallback to anonymous. (The one keyless write is POST /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

  1. Always use the app.grantmaking.ai host. That's where the API lives; the marketing site at grantmaking.ai is a separate host. If you send a request to a host that redirects to the canonical one (e.g. an http:// URL or another alias), HTTP clients strip the Authorization header 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 to https://app.grantmaking.ai.
  2. 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-made apiPath / commentsApiPath values you can call directly. Do not guess at permissions; discover them.
  3. PATCH bodies reject unknown fields. Sending any field not in the allowlists below returns HTTP 400 Unsupported field: .... Send only supported fields.
  4. List responses truncate long text. GET /organizations and GET /projects clip 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.
  5. Respect HTTP 429. On rate limit you get a Retry-After header 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": "..." }:

SituationMessage
No Authorization headerMissing Authorization header
Header isn't Bearer <token>Invalid Authorization header format. Expected: Bearer <token>
Token not foundInvalid API token
Token expiredAPI token has expired
Token revokedAPI 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; editableOrganizations and editableProjects are null because admins can edit everything.
  • access.filtered: true → only the listed entities are writable. Use the provided apiPath / commentsApiPath values directly.
  • token.isReviewer: true (or any admin token) → the key has reviewer scope and can call the Reviewer API. data.capabilities.applications advertises 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 & pathPurposeAccess
GET /meWho am I + what can I editAny valid token
PATCH /me/profileEdit your own profile (and linked public person)Any valid token
GET /organizationsPaginated list of organizationsAnonymous or any valid token
GET /organizations/{id}Read one organization (full text)Anonymous or any valid token
PATCH /organizations/{id}Edit an organizationAdmin, or domain match
GET /organizations/{id}/commentsList comments (anon: count only)Anonymous (count) / admin, domain match
POST /organizations/{id}/commentsPost a comment / replyAdmin, or domain match
DELETE /organizations/{id}/comments/{cid}Delete a comment (soft delete)Admin, or domain-match + own
GET /projectsPaginated list of projectsAnonymous or any valid token
GET /projects/{id}Read one project (full text)Anonymous or any valid token
PATCH /projects/{id}Edit a projectAdmin, or parent-org domain match
GET /projects/{id}/commentsList comments (anon: count only)Anonymous (count) / admin, domain match
POST /projects/{id}/commentsPost a comment / replyAdmin, or parent-org domain match
DELETE /projects/{id}/comments/{cid}Delete a comment (soft delete)Admin, or domain-match + own
GET /tagsTag vocabulary + per-entity usage countsAnonymous or any valid token
POST /feedbackSend feedback or a bug reportPublic (no key needed)
GET /applicationsList submitted applications (reviewer view)Reviewer or admin
GET /applications/{id}Read one full applicationReviewer or admin
PUT /applications/{id}/reviewSet/replace your own score (+ optional comment)Reviewer or admin
DELETE /applications/{id}/reviewRemove your own scoreReviewer or admin
PUT /applications/{id}/claimClaim an application ("I'm reviewing this")Reviewer or admin
DELETE /applications/{id}/claimRelease your claimReviewer or admin
POST /applications/{id}/commentsPost a private comment on the applicant projectReviewer or admin
GET /applications/{id}/commentsRead private comments on the applicant projectReviewer 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):

FieldRequiredNotes
messageyesThe feedback or bug report. Max 8,000 characters.
emailnoA contact address, if you'd like a reply.
sourcenoWhere this came from, e.g. a URL or "API". Max 500 characters.
  • No Authorization header 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. 429 responses carry a Retry-After header (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:

StatusMeaning
400Malformed JSON, invalid UUID, unknown field, or failed validation
401Auth failure (see table above)
403Valid token, but not authorized for this entity or field
404Entity/comment not found, hidden, or not visible to you
409Profile patch needs a linked public person that doesn't exist
429Rate limited — honor the Retry-After header (seconds)
500Server 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-After seconds, 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 .../comments always 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):

ParamTypeBehavior
limit1–100 (default 50)Clamped, not rejected (limit=0 → 1, limit=500 → 100).
offset≥ 0 (default 0)Clamped.
qstring (≤ 100 chars)Case-insensitive substring match on name only (%/_/\ are matched literally).
locationstring (≤ 100 chars)Case-insensitive substring match on the location field.
descriptionContainsstring (≤ 100 chars)Case-insensitive substring match across the description fields (OR). Orgs: descriptionShort/descriptionMedium/descriptionFull; projects add theoryOfChange. Blank → ignored.
tagsslug (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).
tagMatchany | allDefault any. any = entity has at least one of the slugs; all = entity has every slug. Other value → 400.
isActivelyFundraisingtrue/false/1/0Exact match.
updatedAfterISO datetime or YYYY-MM-DDupdatedAt >= value. The main incremental-sync filter.
updatedBeforeISO datetime or YYYY-MM-DDupdatedAt < value; a date-only value means the next UTC midnight (end-exclusive).
sortname | createdAt | updatedAtDefault name. Any other value → 400. id is always appended as a stable tiebreaker.
orderasc | descDefault 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 back null.

GET /organizations

Returns non-hidden organizations. Supports all shared params above, plus:

ParamTypeBehavior
orgTypeenum (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:

ParamTypeBehavior
orgIdUUIDFilter to one organization's projects. Malformed → 400 Invalid orgId format.
orgIdsUUID (multi)IN (...) over org_id; up to 100 ids. Mutually exclusive with orgId — sending both → 400.
statusenum (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.

ParamTypeBehavior
entityTypeorganization | project | person | fundOptional. 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 null clears 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: true redacts 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})

FieldType / rules
namestring, required if present
descriptionShort, descriptionMedium, descriptionFull, theoryOfChangestring or null
websiteUrl, linkedinUrlvalid http/https URL or null
location, fundingStage, fiscalSponsor, trackRecordstring or null
orgTypeknown org type string, or null to clear
foundedDateYYYY-MM-DD or null
teamSize, currentRunwayMonthsinteger ≥ 0 or null
annualBudget, monthlyBurnRate, fundingGoal, fundingRaisedToDatedecimal ≥ 0 or null
isActivelyFundraising, isFundingPrivateboolean
donationLinksarray 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})

FieldType / rules
namestring, required if present
descriptionShort, descriptionMedium, descriptionFull, theoryOfChange, expectedDurationstring or null
websiteUrl, linkedinUrlvalid http/https URL or null
location, fundingStage, fiscalSponsor, trackRecordstring or null
statusone of active, completed, paused, cancelled, upcoming
startDate, endDateYYYY-MM-DD or null (endDate cannot precede startDate)
teamSize, currentRunwayMonthsinteger ≥ 0 or null
annualBudget, monthlyBurnRate, fundingRaisedToDatedecimal ≥ 0 or null
isActivelyFundraising, isFundingPrivateboolean
fundingGoalsobject { minimum?, goal?, stretch? }, each a number ≥ 0, or null
orgIdUUID or null
donationLinksarray 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.

FieldWritten toNotes
nameprofile + personrequired if present
bioprofile + person
displayNameprofileauto-filled from name if you set name without displayName
titleAndOrgprofilemax 120 characters
personalWebsiteUrl, linkedinUrl, twitterUrlpersonvalid http/https URL or null
lesswrongHandle, eaForumHandle, locationpersonstring 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 restricted comment forces the reply to restricted regardless of what you send. The response echoes the stored visibility — 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.

ParamTypeBehavior
limit1–100 (default 50)Clamped, not rejected.
offset≥ 0 (default 0)Clamped.
roundIdUUIDFilter to one funding round. Malformed → 400.
statusstring (optional)Filter by review status.
sortstring (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 of S/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

  1. No API key? You can still do step 2 (read the directory) anonymously at the lower rate limit. Skip /me, writes, and comment contents.
  2. GET /me — learn your scope and editable entities.
  3. To survey the ecosystem: page through GET /organizations and GET /projects with limit=100, then fetch single entities for any rows marked truncated: true that you need in full.
  4. 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.
  5. To leave notes for humans: POST a comment; use "restricted" for internal/reviewer-only notes and "public" otherwise.
  6. 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"
  • Quickstart
  • Anonymous (keyless) access
  • Critical gotchas
  • Authentication
  • Discovery: GET /me
  • Endpoint reference
  • Feedback & bug reports
  • Response and error conventions
  • Rate limiting
  • Reading the directory
  • Editing entities
  • Comments
  • Reviewer API
  • Recommended agent workflow
  • Windows / PowerShell note