Organizations API
Register organizations, read their public profile, team and updates.
The organization's name field is legal_name
An organization has no name field. The registered name is legal_name; trading names live in the dbas collection (dba_name). The verification state is verification_status — a string, not a verified boolean.
List Organizations
GET /api/v1/organizations
Public listing. Only approved and verified, non-soft-deleted organizations are returned — pending, rejected and suspended orgs are never exposed here.
| Query | Notes |
|---|---|
verification_status | Narrows within the public set: approved or verified. Any other value is ignored rather than rejected, and the default public filter still applies. |
country | Exact match on the country field. |
limit | Default 20, clamped to 1–100. |
offset | Default 0. |
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const response = await fetch(`${API_BASE}/organizations?verification_status=verified&limit=20`);
const { data, pagination } = await response.json();{
"data": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"legal_name": "Local Food Bank Inc.",
"dba_name": "Local Food Bank",
"slug": "local-food-bank-a1b2c3",
"country": "US",
"address": { "city": "Austin", "region": "TX" },
"website": "https://localfoodbank.org",
"logo": "https://example.com/logo.jpg",
"banner_image": null,
"description": "Serving families in need since 1985",
"categories": ["community"],
"verification_status": "verified",
"kind": "company",
"parent_organization_id": null,
"mission": "End hunger in our community",
"social_links": {},
"founded_year": 1985,
"verification_completed_at": "2026-02-01T00:00:00Z",
"contact_email": "hello@localfoodbank.org",
"contact_email_public": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-02-01T00:00:00Z"
}
],
"pagination": { "limit": 20, "offset": 0, "total": 42 }
}Those fields are the whole public shape; tax ids, review notes and payment account ids are never returned by a public read. There is no fundraiser_count or total_raised on a listing row — those numbers come from GET /organizations/:id/stats.
contact_email is redacted to null unless contact_email_public is true or the caller is an active member of the organization.
Get Organization
GET /api/v1/organizations/:id
The path parameter accepts either the UUID or the slug. Anything that is neither is a 404, as is a suspended, rejected or pending organization — for anonymous callers. An authenticated member of the organization can also read it in those states (and additionally receives suspension_reason, rejected_reason and suspended_until, which are null for everyone else).
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const response = await fetch(`${API_BASE}/organizations/local-food-bank-a1b2c3`);
const { data } = await response.json();
console.log(data.legal_name, data.verification_status);
console.log(data.dbas, data.primary_location, data.children);The response is { data: { …public fields…, dbas, primary_location, children } }:
dbas—[{ id, dba_name, is_default }], default first.primary_location—{ id, label, address, is_publicly_visible }ornull. The full street is only meant to be rendered whenis_publicly_visibleis true.children— child organizations, populated only whenkindisconglomerate;[]otherwise.
Create Organization
POST /api/v1/organizations 🔒 Session + verified email
Self-service registration. Rate-limited to 5 creations per hour per (IP, user). The new row always lands with verification_status: 'pending' — you cannot self-approve — and the registering user is auto-granted the org_owner role scoped to it.
| Field | Notes |
|---|---|
legal_name | Required. The only required field. |
ein | Must match 12-3456789 when present. |
website, description, country | Optional strings, length-capped. |
categories | Array of non-empty strings. |
kind | company (default) or conglomerate. |
dbas | [{ dba_name }]. The first entry becomes the default. Duplicate names are a 400. |
locations | [{ label?, address }] where address is an object of string fields. |
parent_organization_id is rejected: linking a new org under an existing conglomerate goes through the org-admin surface, which can authorize against the parent.
const response = await fetch(`${API_BASE}/organizations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
legal_name: 'Local Food Bank Inc.',
ein: '12-3456789',
website: 'https://localfoodbank.org',
description: 'Serving families in need since 1985',
country: 'US',
categories: ['community'],
dbas: [{ dba_name: 'Local Food Bank' }],
locations: [{ label: 'HQ', address: { line1: '100 Main St', city: 'Austin', region: 'TX' } }]
})
});
const { data } = await response.json();
console.log('Created, pending verification:', data.id, data.slug);Returns 201 with { data: { id, legal_name, kind, parent_organization_id, slug, verification_status, created_at, dbas, locations, … } }. The slug is generated server-side from the legal name — you do not supply one. A validation problem is a 400 carrying the specific message; a duplicate is a 409.
Deleting an organization
DELETE /organizations/:id no longer exists and answers 404. To close an organization, contact FundlyHub support; deletion is handled by FundlyHub staff, with a grace period during which it can be restored.
Organization statistics
GET /api/v1/organizations/:id/stats
Accepts a UUID or a slug. Responds with camelCase fields at the top level — there is no data wrapper:
{
"campaignCount": 4,
"totalCampaignCount": 7,
"totalFundsRaisedCents": 45600000,
"uniqueDonorCount": 312,
"followerCount": 88
}campaignCount counts only active campaigns; totalCampaignCount counts every campaign ever attached to the org. totalFundsRaisedCents sums paid donations from donations.amount_cents, so it is integer cents — divide by 100 before you display it. Donor uniqueness falls back to donor_email for guests, so guest gifts count without double-counting.
Team members
GET /api/v1/organizations/:id/members
Read-only, public, UUID-or-slug. Returns { data: [...] } of publicly-visible active memberships: user_id, name, profile_slug, avatar, kyc_verified_at, org_role_title_id, role_title_label, role_title_category, role_title_sort_order, start_date, end_date, is_current, affiliation_description, rbac_role_name, rbac_hierarchy_level.
Members whose profile visibility is private, whose membership is hidden or expired, or who hold a plain org_viewer role with no role title are omitted.
There is no POST /organizations/:id/members
Membership is not managed through this endpoint family — adding, changing or removing a member goes through the org-admin surface, which authorizes per-organization. A POST here returns 404.
Updates and documents
| Endpoint | Purpose |
|---|---|
GET /organizations/{id}/updates | Public updates feed, newest first, offset-paginated (limit clamped to 1–50). |
GET /organizations/{id}/documents/public | Documents an admin has explicitly flagged public on a verified org. |
GET /organizations/me | The organizations the authenticated caller holds an active role on. Flat array, no pagination. |
GET /org-role-titles | The catalogue of public team positions (CEO, Volunteer, …) used on team lists. |
POST /organizations/:id/report | Report an organization to the moderators (verified email). |
Response Codes
200— Success201— Created (pending verification)400— Validation error, or no valid fields to update401— Authentication required403— Permission denied404— Not found, or not publicly visible409— Organization already exists429— Rate limit exceeded (5 registrations/hour)500— Server error