Categories API
Read the fundraising category taxonomy and its per-category statistics. All four endpoints are public and read-only — there is no create, update or delete on this resource.
Category ids are integers
categories.id is an integer, not a UUID. Every path parameter below accepts either that integer id or the category slug: a value made only of digits is looked up by id, anything else by slug.
List Categories
GET /api/v1/categories
All active categories, ordered by display_order.
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const response = await fetch(`${API_BASE}/categories`);
const { data } = await response.json();{
"data": [
{
"id": 3,
"slug": "medical",
"name": "Medical",
"description": "Healthcare and medical emergency campaigns",
"icon": "local_hospital",
"color": "#E53935",
"display_order": 1
}
]
}Those seven keys are the whole projection. There is no emoji field (the field is icon), and no fundraiser_count or total_raised_cents — counts come from the stats endpoints below, which are separate queries for a reason.
Get a single Category
GET /api/v1/categories/:id
By integer id or by slug. Returns { data: { … } } with the same seven fields as the listing, or 404 if the category does not exist or is inactive.
const response = await fetch(`${API_BASE}/categories/medical`);
const { data } = await response.json();
console.log(data.name, data.icon);Statistics for one category
GET /api/v1/categories/:id/stats
const response = await fetch(`${API_BASE}/categories/medical/stats`);
const { data } = await response.json();{
"data": {
"category_name": "Medical",
"organization_count": 6,
"fundraiser_count": 145,
"total_raised_cents": 284750000
}
}fundraiser_count counts active campaigns only, so it matches what a visitor sees in the listing. total_raised_cents sums paid donations from donations.amount_cents, so the figure is integer cents — divide by 100 before you display it.
Statistics for every category
GET /api/v1/categories/stats
One row per active category, in display_order. Returns an array under data — not a summary object.
const response = await fetch(`${API_BASE}/categories/stats`);
const { data } = await response.json();
const topByAmount = [...data]
.sort((a, b) => b.total_raised_cents - a.total_raised_cents)
.slice(0, 5);
topByAmount.forEach((cat, i) => {
console.log(`${i + 1}. ${cat.category_name}: $${(cat.total_raised_cents / 100).toLocaleString()}`);
});{
"data": [
{
"category_name": "Medical",
"category_slug": "medical",
"organization_count": 6,
"campaign_count": 145,
"total_raised_cents": 284750000
}
]
}The two stats endpoints do not use the same key
The per-category endpoint returns fundraiser_count; the all-categories endpoint returns campaign_count for the same number, and adds category_slug. Neither returns platform totals such as total_categories or total_fundraisers — sum the array yourself if you need them.
Fundraisers in a category
There is no /categories/:slug/fundraisers route. Filter the campaign listing instead — category accepts the integer id, the slug, or the display name, and the resolver also matches the singular form of either:
const response = await fetch(
`${API_BASE}/fundraisers?category=medical&status=active&limit=20`
);
const { data, pagination } = await response.json();
console.log(`Found ${pagination.total} medical fundraisers`);See Fundraisers for the full parameter list and the response shape.
Building a category menu
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const [{ data: categories }, { data: stats }] = await Promise.all([
fetch(`${API_BASE}/categories`).then(r => r.json()),
fetch(`${API_BASE}/categories/stats`).then(r => r.json())
]);
const countBySlug = Object.fromEntries(
stats.map(s => [s.category_slug, s.campaign_count])
);
categories.forEach(cat => {
console.log(`${cat.name} (${countBySlug[cat.slug] ?? 0})`);
});Response Codes
200— Success404— Category not found or inactive429— Rate limit exceeded500— Server error