Organization Admin (/org-admin)
The /org-admin/:slug/* surface is how an organization's own team runs it: members, settings and branding, DBAs and locations, verification documents, child companies, Stripe payouts, updates, and read-only views of fundraisers, donations and donors. It is the API behind fundlyhub.org/org-admin/<slug>.
It is the organization's own surface. Verification, suspension and other lifecycle decisions are made by FundlyHub and are not part of this API.
| Reference section | Operations | About |
|---|---|---|
| Organization Admin | 34 | The org-scoped admin panel at `/org-admin/{slug}/*`. Every operation needs a bearer session, resolves `{slug}` to an organization (an unknown or malformed slug answers `404 Organization not found`, never `403`), and then checks an **org-scoped permission** held through the caller's `org_owner`, `org_admin` or `org_viewer` role on that organization. A caller without the permission gets `403` with `message: "Requires permission: <name>"`. |
How access works
Every route needs a signed-in caller and an org-scoped permission. The API:
- Resolves
:slugto an organization. A malformed or unknown slug is404 Organization not found— never403, so the endpoint does not confirm that a slug exists. - Checks the permission in the organization scope: your role assignments on that organization.
A missing permission is 403 with message: "Requires permission: org.…". To find out up front what a user may do, call GET /me/capabilities?scopeType=organization&scopeId=<org-uuid>.
Organization roles
| Permission | org_viewer | org_admin | org_owner |
|---|---|---|---|
org.read, org.read_donations, org.read_donors, org.read_payouts | ✓ | ✓ | ✓ |
org.update_settings, org.manage_members, org.manage_dbas, org.manage_locations, org.upload_documents, org.export_csv | ✓ | ✓ | |
org.manage_payouts (connect Stripe) | ✓ | ||
org.manage_children (create child companies) | ✓ |
The registering user becomes org_owner automatically.
Endpoints
Overview and reads
| Endpoint | Permission | Notes |
|---|---|---|
GET /org-admin/:slug/overview | org.read | KPIs. For a conglomerate the totals roll up its child companies. |
GET /org-admin/:slug/fundraisers | org.read | ?status=, ?limit=, ?offset=. |
GET /org-admin/:slug/donations | org.read_donations | ?status=, ?fundraiser_id=, paging. |
GET /org-admin/:slug/donors | org.read_donors | Distinct donors with totals. |
Each donor row has an opaque donor_key: user:<profile id> for a signed-in donor, guest:<24 hex characters> for a guest, or anonymous for the single row that collects every anonymous gift. It is stable across pages, so use it to tell rows apart; it carries no contact details.
Members
| Endpoint | Permission | Body |
|---|---|---|
GET /org-admin/:slug/members | org.manage_members | — |
GET /org-admin/:slug/users/search?q= | org.manage_members | Confirm the account behind a complete email address before adding it. |
POST /org-admin/:slug/members | org.manage_members | { email, role, org_role_title_id?, custom_role_title? } |
PATCH /org-admin/:slug/members/:userId | org.manage_members | { role } |
DELETE /org-admin/:slug/members/:userId | org.manage_members | — |
- Only an approved or verified organization can look up or add members. Otherwise both
users/searchandPOST …/membersanswer403withcode: "ORG_NOT_APPROVED". users/searchtakes a complete email address inq, matched case-insensitively against account login emails. It answers{ "results": [ … ] }with at most one entry,{ id, type: "user", name, avatar, masked_email }, wheremasked_emaillooks likes***@example.org. Anything that is not a full address returns an empty list; this is not a directory search.- Lookups and adds share a limit of 30 requests per minute per user; over it is
429. - The person must already have a FundlyHub account: an unknown email is
404 "No FundlyHub user with that email. Ask them to sign up first."There is no email invitation for people without an account. An existing member is409. - A new member's
roleisorg_adminororg_viewer.org_owneris reached only by promotion withPATCH, and only an owner can promote to owner. - Roles rank
org_owner>org_admin>org_viewer. Only an owner can change or remove another owner. Nobody can change or remove a member whose role is equal to or above their own, or grant a role above their own. Each of these is403. Stepping down or leaving yourself is always allowed. - The only owner cannot be demoted or removed (
400) — promote someone else first. custom_role_title(up to 100 characters) ororg_role_title_idis the public position shown on the team list;GET /org-role-titleslists the catalogue.
Settings, branding and updates
| Endpoint | Permission | Notes |
|---|---|---|
PATCH /org-admin/:slug/settings | org.update_settings | legal_name, website, description, country, logo, banner_image, mission, contact_email, contact_email_public, founded_year, social_links. Fields you omit are left alone. |
POST / DELETE /org-admin/:slug/avatar | org.update_settings | Organization logo (base64 JSON, like the user avatar). |
POST / DELETE /org-admin/:slug/banner | org.update_settings | Banner image. |
POST /org-admin/:slug/updates | org.update_settings | { title, body, cover_image? } — a post on the public profile's "Latest news". |
PATCH / DELETE /org-admin/:slug/updates/:updateId | org.update_settings | Edit or delete a post. |
The public profile is read through /organizations/*, a different surface from these writes. FundlyHub's web client expires its cached /organizations/* reads whenever an /org-admin/* write succeeds; a client of your own that caches the public profile should do the same, or a saved change keeps showing the old copy until its cache expires.
DBAs and locations
| Endpoint | Permission | Body |
|---|---|---|
GET /org-admin/:slug/dbas | org.read | — |
POST /org-admin/:slug/dbas · PATCH …/:dbaId · DELETE …/:dbaId | org.manage_dbas | { dba_name, is_default? } |
GET /org-admin/:slug/locations | org.read | — |
POST /org-admin/:slug/locations · PATCH …/:locationId · DELETE …/:locationId | org.manage_locations | { label?, address, is_primary?, is_publicly_visible? } |
The default DBA is the organization's public display name. Only the primary location is shown on the public profile.
Verification documents
| Endpoint | Permission | Notes |
|---|---|---|
GET /org-admin/:slug/documents | org.read | Every document and its review status (financial documents need more; see below). |
POST /org-admin/:slug/documents | org.upload_documents | { doc_type, content_type, original_filename?, file_base64 } |
GET /org-admin/:slug/documents/:docId/download | org.read | — |
DELETE /org-admin/:slug/documents/:docId | org.upload_documents | — |
PATCH /org-admin/:slug/documents/:docId/public-visibility | org.upload_documents | { is_publicly_visible } — show an approved document on the public profile. |
doc_type is one of ein_letter, 501c3_determination, w9, voided_check, other; content_type is application/pdf, image/png or image/jpeg; the decoded file is at most 10 MB. Uploading a new document of a type supersedes the previous one. An approved ein_letter or501c3_determination is what verification requires. FundlyHub reviews each uploaded document.
Financial documents — w9, voided_check and other — are visible only to callers who also hold org.upload_documents (org_admin and org_owner). For anyone else, such as an org_viewer, they are left out of the list, and downloading one answers 404. ein_letter and 501c3_determination need org.read only.
Child companies (conglomerates)
| Endpoint | Permission | Notes |
|---|---|---|
GET /org-admin/:slug/children | org.read | — |
POST /org-admin/:slug/children | org.manage_children + verified email | Registers a company under this conglomerate. The parent must be kind: conglomerate; the hierarchy is two levels deep. |
Payouts
| Endpoint | Permission | Notes |
|---|---|---|
POST /org-admin/:slug/payouts/connect | org.manage_payouts | Creates the organization's Stripe Connect account if needed and returns 201 { data: { accountId, onboardingUrl, status } }; send the user to onboardingUrl. The organization must be verified (400 otherwise). |
GET /org-admin/:slug/payouts/status | org.read_payouts | Charges/payouts enabled, country, currency, and what Stripe still needs. |
Donations to the organization's fundraisers pay out to this account, separately from any member's personal payouts. See Payouts.
Example
# What may I do in this organization?
curl -b cookies.txt \
"https://api.fundlyhub.org/api/v1/me/capabilities?scopeType=organization&scopeId=$ORG_ID"
# Add a teammate who already has an account
curl -X POST -b cookies.txt -H 'Content-Type: application/json' \
https://api.fundlyhub.org/api/v1/org-admin/local-food-bank-a1b2c3/members \
-d '{ "email": "sam@example.com", "role": "org_viewer", "custom_role_title": "Volunteer coordinator" }'Auditing
Related
- Organizations API — the public reads and self-service registration
- Setup & verification and Running your organization — the user guides