API Reference

Every call the GrantsHub API makes available. Every endpoint except /v1/match/vocab needs an X-API-Key header — get yours from your developer dashboard after registering.

/v1/match

Starter and above

Send an organisation profile, get ranked grant matches back with a fit score and reasons. The score says how closely the grant fits your description: mostly how near your description is to what the grant funds, nudged up a little when the sector, amount or key words match and down when the grant is a poor fit for you. It ranks results; it is not a chance of winning. Strong matches usually score about 0.5 to 0.75, and a score above 0.8 is rare. A longer, specific description gives clearer scores. Optional min_score: only return grants scoring at least this much (0 to 1), so set it with that range in mind. Leave it out to get the best matches whatever their score. Optional filters: leave out grants that rule your organisation out, by who can apply, area down to council, what the money is for, who it helps and more (see Matching filters below). Each match carries a master_id: pass it as grant_id to /v1/score or /v1/proposal to go deeper on that grant. Each match also carries match_fields, the facts behind the filters for that grant.

POST /v1/match
curl https://api.grantshub.co.uk/v1/match \
  -H "X-API-Key: gh_..." \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_type": "charity",
    "sector": "Arts / Culture",
    "region": "Scotland",
    "description": "Community theatre charity seeking funding for a youth outreach programme.",
    "amount_needed": 15000,
    "top_n": 5,
    "filters": {
      "applicant_type": "charity",
      "nation": "scotland",
      "purposes": ["arts", "youth"],
      "beneficiary_groups": ["young_people"],
      "partner_ok": false,
      "money_only": true
    }
  }'

# → 200 OK
{
  "matches": [
    {
      "title": "Scottish Arts Community Fund",
      "funder": "Creative Scotland",
      "amount_min_gbp": 5000,
      "amount_max_gbp": 15000,
      "amount_text": "£5,000 - £15,000",
      "open_date": "2026-04-01",
      "close_date": "2026-09-30",
      "eligibility": "Registered charities and community groups in Scotland.",
      "url": "https://www.creativescotland.com/funding/...",
      "score": 0.68,
      "reasons": ["Sector match: Arts / Culture", "Region match: Scotland", "Within funding range"],
      "master_id": "a1b2c3d4e5f6",
      "match_fields": {
        "applicant_types": ["charity", "community_group"],
        "area_scope": "scotland",
        "regions": [],
        "local_area_codes": [],
        "purposes": ["arts", "youth", "community"],
        "beneficiary_groups": ["young_people"],
        "funding_form": "grant",
        "needs_partner": false
      }
    }
  ],
  "distinct_grant_cap_notice": null
}

Regions & sectors

The region and sector values are closed-vocabulary — send one of the exact values below (case-sensitive). Every endpoint rejects anything else with a 400, and a rejected call does not count towards your monthly quota.

Regions

UK-wideEnglandScotlandWalesNorthern IrelandLondonSouth EastSouth WestNorth WestNorth EastYorkshireEast MidlandsWest MidlandsEast of EnglandInternational

Sectors

Arts / CultureDefence / SecurityAgriculture / FoodSustainability / EnvironmentEducation / SkillsCommunity / SocialBusiness / InnovationHealth / Science / ResearchTechnology / DigitalGeneral / Other

Matching filters

Add a filters object to a /v1/match call to leave out grants that rule your organisation out. Every filter is optional and only applies when you send it, so a call without filters behaves exactly as before. The allowed values are listed below the table, read live from GET /v1/match/vocab (no key needed), so they are always the ones the API checks. Anything else gets a 400 naming the field and its allowed values.

A grant that doesn't say is kept.If a funder's page doesn't state who can apply, where, or who it helps, the grant stays in your results. A grant is only left out when what it states clashes with what you sent, for example Kent only when you sent a Leeds council.

FilterWhat it does
applicant_typeone valueWho is applying. Leaves out grants for other kinds of applicant. A charity also reaches grants for community groups.
nationone valueLeaves out grants limited to another nation or to regions outside it.
regionone valueLeaves out regional and local grants for other regions. Must sit inside nation if you send both.
councilsup to 20 codesLeaves out local grants that name only other councils. UK-wide, national and regional grants are never removed by a council. Sets the region and nation when you don't send them.
purposesup to 8 valuesWhat the money is for. Leaves out grants whose stated purposes meet none of yours.
sectorsup to 5 valuesYour industry. Used with purposes to judge research and challenge-led grants set for a particular industry.
technologiesup to 5 valuesFor research and development: the technologies you work in, used the same way as sectors.
beneficiary_groupsup to 8 valuesWho your work helps. Leaves out grants limited to other groups. Children and young people count as one family.
partner_oktrue / falsefalse leaves out grants that need a partner organisation.
money_onlytrue / falsetrue leaves out support without money, access to facilities and equity investment.

Allowed values

Send the code on the left of each item. Labels are for reading only.

Loading the allowed values…

Limits & errors

Dev Starter and Dev Pro have three limits: calls per month, results per call, and distinct grants per month. A grant counts once, the first time any endpoint or webhook shows it to your account. After that it is free to see again, in any month. Calls and distinct grants both reset on the 1st of each month. Only successful calls count towards your monthly calls. Your developer dashboard shows how much of each you have used. Enterprise gives you the full grants database and unlimited matching under a signed data licence, with no distinct-grant limit.

StatusWhen
401Missing, invalid or revoked X-API-Key.
403The endpoint is not on your plan (e.g. /v1/proposal on Starter), your plan is inactive, or every grant in the response would be new and your distinct-grant limit is used up.
400 / 422A parameter is missing or invalid, e.g. a region or sector not in the list above. The detail field says which.
404The grant_id does not exist.
429Monthly call limit reached. The message gives the reset date.

When some results are held back because of the distinct-grant limit, you still get a 200 with the grants you can see, and distinct_grant_cap_notice says how many were held back.

We keep the details you send with each call - such as sector, region, description and amount - for 30 days, so we can look into a result with you if something doesn't look right and keep improving matches. After 30 days they are deleted. Your API key is never stored with them.

Webhooks

Pro and Enterprise only

Add a webhook in your developer dashboard with an https URL, an optional sector and region, and a match threshold. Once a day we check the grants added since its last delivery against it, then POST any that score at or above the threshold. Grants sent by webhook count towards your distinct-grant limit like any other response. Return any 2xx status to confirm delivery.

POST https://your-app.com/webhook
Content-Type: application/json
X-GrantsHub-Signature: sha256=5d41402abc4b2a76b9719d911017c592...

{
  "event": "new_match",
  "webhook_id": 42,
  "grants": [
    {
      "master_id": "a1b2c3d4e5f6",
      "title": "Scottish Arts Community Fund",
      "funder": "Creative Scotland",
      "close_date": "2026-12-15",
      "url": "https://www.creativescotland.com/funding/...",
      "score": 82.5
    }
  ]
}

Verifying the signature

Every delivery is signed with the webhook's signing secret, which you can see in your dashboard. X-GrantsHub-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body. Compute it over the bytes exactly as received, before parsing the JSON, and compare in constant time.

Python
import hmac, hashlib

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header or "")
Node.js
const crypto = require('crypto');

function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}