Roles & Permissions
FundlyHub uses role-based access control. Roles are assigned to users, permissions are assigned to roles, and protected endpoints name the permission they require — never the role.
A protected endpoint answers:
401when the request carries no valid credential (a session cookie, a Cognito ID token or an API key — see Authentication).403when the caller is signed in but their effective permission set for the request's scope does not contain the permission. The body names what was missing:
{
"error": "Forbidden",
"message": "Requires permission: endorse_campaigns",
"required_permission": "endorse_campaigns"
}GET /permissions/check has been removed
Removed endpoint
GET /permissions/check no longer exists, and any request to it returns 404. Use GET /me/capabilities, which answers the same question for the calling user.
GET /me/capabilities
Returns the authenticated caller's own effective permissions and roles. This is the correct way for a client to decide what to show or attempt.
Auth: signed in. No permission is required — every authenticated caller may read their own capabilities.
Query parameters
| Parameter | Required | Description |
|---|---|---|
scopeType | No | One of global, organization, fundraiser. Defaults to global. Any other value returns 400. |
scopeId | Conditional | The organization or fundraiser UUID. Required whenever scopeType is not global; omitting it returns 400. |
Example
curl -b cookies.txt \
"https://api.fundlyhub.org/api/v1/me/capabilities?scopeType=global"{
"permissions": ["create_fundraiser", "edit_own_fundraiser"],
"roles": ["creator"],
"scope": { "type": "global", "id": null },
"fetchedAt": "2026-09-04T12:00:00.000Z",
"expiresAt": "2026-09-04T12:05:00.000Z"
}permissions is the flat list of effective permission names. roles is the list of role names held in that scope. Global assignments always apply, so a global role shows up in an organization-scoped or fundraiser-scoped response too.
expiresAt is a cache hint, not a session deadline
Permission sets are cached briefly. expiresAt tells a client when its copy should be considered stale. It says nothing about how long the caller's session is valid.
Roles a client will see
| Role | Scope | Notes |
|---|---|---|
user | Global | Every account |
donor | Global | Platform donor |
creator | Global | Fundraiser creator; granted, for example, when a first creator tier is created |
verified_creator | Global | Verified fundraiser creator |
ambassador | Global | Referral partner: view_own_referral_portal and endorse_campaigns — see Ambassadors |
org_owner | Organization | Auto-granted on organization registration |
org_admin | Organization | Manage org members, settings and payouts |
org_viewer | Organization | Read-only access to an organization's admin panel |
FundlyHub's own staff roles also exist; they are not part of this public API. The organization roles and their org.* permissions are described on Organization Admin.
Roles do not inherit each other's permissions
A role holds exactly the permissions mapped to it — not the union of everything held by roles below it. Always check for the permission an action needs (from GET /me/capabilities), never for a role name.
Scope
A permission is checked in the scope of the request: an organization's routes check the organization scope, a campaign's routes may check the fundraiser scope, and everything else is global. Global assignments apply in every scope; a scoped assignment applies only in its own scope.
Error responses
| Code | Meaning |
|---|---|
400 | Validation error — for example an invalid scopeType or a missing scopeId on /me/capabilities |
401 | No authentication token was presented |
403 | Authenticated, but missing the required permission — see required_permission |
404 | Not found |