Notifications & Campaign Updates
Two in-app feeds belong to every signed-in account:
- Notifications — things that happened to you or your campaigns (a donation, a status change, a new follower).
- Campaign updates — the news posts creators publish on campaigns you follow or gave to, each marked read or unread.
The app's badge is the sum of the two unread counts, and one request returns both: GET /notifications answers unreadCount and updatesUnreadCount. Read state for both is kept on the server, so it is the same on every device.
All endpoints on this page take authenticateToken and act on the caller.
| Reference section | Operations | About |
|---|---|---|
| Notifications | 9 | In-app notifications for the authenticated user. Which events also generate email is governed by the toggles on `/users/{id}/preferences`, not by anything under `/notifications`. |
| Social | 6 | Following users and organizations |
| Updates | 12 | Project updates, milestones, and funding stats |
Notifications
| Endpoint | Body / query | Returns |
|---|---|---|
GET /notifications | ?limit= (default 20, max 100), ?archived=true for the archive | { notifications, unreadCount, updatesUnreadCount } |
PATCH /notifications/mark-read | { "ids": ["…"] } | { success, updated } |
PUT /notifications/read-all | — | { success, updated } |
PUT /notifications/:id/read | — | { success, updated } |
PATCH /notifications/archive | { "ids": ["…"] } | { success, archived } |
DELETE /notifications | { "ids": ["…"] } | { success, deleted } |
ids must be a non-empty array, or the response is 400 ids array is required. Every write is filtered by your user id, so ids that are not yours are ignored rather than refused — which is why the answers carry a count. Archiving also marks the notification read. There is no "mark unread".
message can contain a small amount of HTML (a link to the campaign); the interpolated values are escaped server-side.
curl -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
"https://api.fundlyhub.org/api/v1/notifications?limit=20"{
"notifications": [
{
"id": "…",
"type": "donation_received",
"category": "donation",
"priority": "high",
"title": "New donation!",
"message": "Jane Smith donated $50.00 to <a href=\"…\">Help the Riverside Library</a>.",
"icon": "Heart",
"action_url": "/f/help-the-riverside-library",
"action_label": null,
"metadata": { },
"is_read": false,
"is_archived": false,
"created_at": "2026-10-01T18:22:04.120Z"
}
],
"unreadCount": 3,
"updatesUnreadCount": 2
}unreadCount counts unread, unarchived notifications. updatesUnreadCount is the campaign-updates count described below; it is computed alongside and is null (not 0) if that count failed, so a client can keep the last value it had instead of clearing the badge.
Campaign updates
What is in the feed
An update appears in your feed when it was posted on a campaign that you are connected to and that is not your own:
| You… | The campaign must be | Counted from |
|---|---|---|
| Follow its holder — the person who runs it, or the organization it belongs to | Live (active or ended), public, not deleted | When you followed |
| Gave to it (a paid gift under your account) | Readable by link: unlisted and paused stay in; private and unapproved do not | Your first gift |
There is no campaign-level follow: you follow a person or an organization with POST /subscriptions ({ "following_id": "…", "following_type": "user" | "organization" }), and their campaigns' updates come with it. Updates you wrote yourself, and updates that were deleted, never appear.
Only what is posted after the relationship began counts as unread. Older updates on the same campaign are in the feed as history, already read. So following a prolific creator, or giving to a campaign with forty updates, adds nothing to the badge. Unfollowing and following again moves the starting point forward.
Endpoints
| Endpoint | Body / query | Returns |
|---|---|---|
GET /me/campaign-updates | ?limit= (default 20, max 50), ?cursor=, ?lang= | { data, next_cursor, unread_count } |
PUT /me/campaign-updates/:updateId/read | — | { success, updated, unread_count } |
PUT /me/campaign-updates/read-all | Optional { "until": "<ISO-8601>" } | { success, updated, unread_count } |
Responses are Cache-Control: private, no-store.
Paging is by keyset, not offset: pass the next_cursor from one page as ?cursor= on the next. next_cursor is null on the last page. A malformed cursor is a 400 Invalid cursor.
Each item is the update row plus type: "update", is_read, read_at, author: { id, name, avatar } and fundraiser: { id, title, slug, cover_image }. With ?lang=en|ru|uk|es the title and body are overlaid with the translation in that language where one exists.
Marking read. Every write answers the new unread_count, so the badge can move without a second request. Marking one update read answers 404 if the update does not exist (or was deleted) and 400 if the id is not a UUID; marking an already-read update is a success with updated: 0.
read-all takes an optional until. Send the timestamp of the newest update you are showing, and only updates posted at or before it are marked — so an update that arrived after you fetched the list stays unread. Without until, everything unread is marked.
curl -X PUT -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
-H 'Content-Type: application/json' \
https://api.fundlyhub.org/api/v1/me/campaign-updates/read-all \
-d '{ "until": "2026-10-01T18:22:04.120Z" }'{ "success": true, "updated": 2, "unread_count": 0 }Reading an update is a change to your own rows only: it publishes no event and writes no audit entry.
Posting an update (creators)
A campaign's owner posts updates with POST /projects/:fundraiserId/updates ({ "title"?: "…", "body": "…" } — body required, up to 20,000 characters; title up to 200; requires a verified email and the features.project_updates flag). PATCH /projects/:fundraiserId/updates/:updateId edits one and DELETE withdraws it. The public list is GET /projects/:fundraiserId/updates.
A new update is translated into the other three languages in the background and appears, unread, in the campaign-updates feed of everyone connected to the campaign as described above.
Signed-in readers can like an update (and any comment) with PUT/DELETE /projects/:fundraiserId/updates/:updateId/like; the public list carries like_count and liked_by_me on every update. The feed on this page does not — see Comments, Updates & Likes.
Related
- Comments, Updates & Likes — comments, the public update list and likes
- Following, campaign updates & likes — the user guide
- Email Notifications — the email side, and the
notify_*preferences on User Profiles - Account (/me)