Skip to content

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 sectionOperationsAbout
Notifications9In-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`.
Social6Following users and organizations
Updates12Project updates, milestones, and funding stats

Notifications ​

EndpointBody / queryReturns
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.

bash
curl -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
  "https://api.fundlyhub.org/api/v1/notifications?limit=20"
json
{
  "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 beCounted from
Follow its holder — the person who runs it, or the organization it belongs toLive (active or ended), public, not deletedWhen you followed
Gave to it (a paid gift under your account)Readable by link: unlisted and paused stay in; private and unapproved do notYour 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 ​

EndpointBody / queryReturns
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-allOptional { "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.

bash
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" }'
json
{ "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.

Built with VitePress