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.
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.
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
}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.
UK-wideEnglandScotlandWalesNorthern IrelandLondonSouth EastSouth WestNorth WestNorth EastYorkshireEast MidlandsWest MidlandsEast of EnglandInternationalArts / CultureDefence / SecurityAgriculture / FoodSustainability / EnvironmentEducation / SkillsCommunity / SocialBusiness / InnovationHealth / Science / ResearchTechnology / DigitalGeneral / OtherAdd 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.
| Filter | What it does |
|---|---|
| applicant_typeone value | Who is applying. Leaves out grants for other kinds of applicant. A charity also reaches grants for community groups. |
| nationone value | Leaves out grants limited to another nation or to regions outside it. |
| regionone value | Leaves out regional and local grants for other regions. Must sit inside nation if you send both. |
| councilsup to 20 codes | Leaves 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 values | What the money is for. Leaves out grants whose stated purposes meet none of yours. |
| sectorsup to 5 values | Your industry. Used with purposes to judge research and challenge-led grants set for a particular industry. |
| technologiesup to 5 values | For research and development: the technologies you work in, used the same way as sectors. |
| beneficiary_groupsup to 8 values | Who your work helps. Leaves out grants limited to other groups. Children and young people count as one family. |
| partner_oktrue / false | false leaves out grants that need a partner organisation. |
| money_onlytrue / false | true leaves out support without money, access to facilities and equity investment. |
Send the code on the left of each item. Labels are for reading only.
Loading the allowed values…
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.
| Status | When |
|---|---|
| 401 | Missing, invalid or revoked X-API-Key. |
| 403 | The 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 / 422 | A parameter is missing or invalid, e.g. a region or sector not in the list above. The detail field says which. |
| 404 | The grant_id does not exist. |
| 429 | Monthly 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.
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.
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
}
]
}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.
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 "")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);
}