Skip to content

Comments, Updates & Likes ​

The social layer under a campaign: the comments people leave, the updates its creator posts, and the likes on both. This page covers the public reads, the writes, and the two like fields that every list carries.

Following people and organizations, and the per-account feed of updates with its read state, are on Notifications & Campaign Updates.

Reference sectionOperationsAbout
Social6Following users and organizations
Comments7Fundraiser comments and replies
Updates12Project updates, milestones, and funding stats

Comments ​

EndpointAuthPurpose
GET /fundraisers/:fundraiserId/commentsOptionalThe campaign's visible comments, newest first. ?limit= (default 50, max 100), ?offset=.
POST /fundraisers/:fundraiserId/commentsSession + verified email; flag features.comments{ "content": "…", "parent_comment_id"?: "…", "gif_id"?: "…" }. content is trimmed and must be 1–2,000 characters, or may be empty when gif_id is set (GIFs). A reply's parent must be on the same campaign.
DELETE /comments/:idSession; your own commentRemoves it. A comment tied to a donation is retracted rather than deleted: it leaves every public surface but keeps its claim on the gift.

The list answers { data: [...], pagination: { limit, offset, total } }. It shows only comments that are not hidden by a moderator and not retracted, on a campaign a visitor could open: not deleted, status active, ended or paused, and not private. Comments on any other campaign are not readable here, even by id.

POST answers 201 with { data: comment }, already carrying like_count: 0 and liked_by_me: false, so a client can prepend it to the list without a refetch.

Every comment row, in the list, in the POST answer and in the admin listings, carries gif: a GIF object or null.

Donors can also leave a note on their own gift through the receipt — see Donations. A receipt note cannot carry a GIF. FundlyHub moderators can hide a comment; a hidden comment is left out of every list.

GIFs ​

A comment or a reply can carry one GIF from GIPHY, with or without text. The client asks the API for GIFs, the reader picks one, and the comment is posted with that GIF's gif_id. Clients never send URLs: the server looks the id up on GIPHY again and stores the GIF object it builds itself.

EndpointAuthPurpose
GET /gifs/trendingOptionalTrending GIFs. ?limit= (default 24, max 50), ?offset=.
GET /gifs/searchOptional?q= (required), ?limit=, ?offset=, ?lang=en|ru|uk|es.

Both answer { "data": [Gif, …], "next_offset": 24 }. For the next page, pass next_offset as offset. It is null on the last page. Results are rated pg or lower and cached on the server: trending for five minutes, and searches for ten. Both reads, and a comment POST that carries gif_id, share a limit of 60 a minute per account (Rate Limits).

The GIF object, on every comment row as gif (or null):

json
{
  "id": "xT4uQulxzV39haRFjG",
  "title": "Happy Dance GIF",
  "width": 480, "height": 270,
  "mp4_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.mp4",
  "webp_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.webp",
  "gif_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.gif",
  "still_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy_s.gif",
  "preview_mp4_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/200w.mp4",
  "preview_webp_url": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/200w.webp",
  "preview_width": 200, "preview_height": 113
}

Every URL is https:// on a giphy.com subdomain, with no query string. The full size is GIPHY's original, the still is original_still, and the preview is the 200 px wide fixed_width. Show title as text (alt text), never as HTML. Play mp4_url muted and looped with still_url as the poster; under reduced motion, show the still. Show GIPHY's attribution ("Powered by GIPHY") in your picker.

StatusMeaning
503 { "error": "gifs_unavailable" }GIF comments are off (features.comment_gifs) or GIPHY is not configured. Hide the GIF button. A POST with gif_id gets it too.
502 { "error": "gifs_upstream" }GIPHY failed. Retry later.
400 { "error": "invalid_query" }GET /gifs/search without a usable q.
400 { "error": "Invalid gif_id" }gif_id is not a GIPHY id (^[A-Za-z0-9]{6,40}$).
422 { "error": "gif_not_found" }GIPHY has no GIF with that id.
422 { "error": "gif_not_allowed" }The GIF is rated above pg.

A GIF is part of its comment. Hiding, retracting or deleting the comment hides or removes the GIF, and the comment's text and GIF are never moderated separately.

Campaign updates ​

EndpointAuthPurpose
GET /projects/:fundraiserId/updatesOptionalThe campaign's update feed, newest first.
POST /projects/:fundraiserId/updatesOwner; verified email; flag features.project_updates{ "title"?: "…", "body": "…" } — body required, up to 20,000 characters; title up to 200. Longer is 413.
PATCH /projects/:fundraiserId/updates/:updateIdOwner; verified email; flag features.project_updatesEdit the title or body.
DELETE /projects/:fundraiserId/updates/:updateIdOwnerWithdraw it.

The feed is a bare JSON array, not wrapped in data. It interleaves two kinds of item by date, told apart by type:

  • type: "update" — an update row with author: { id, name, avatar }, like_count and liked_by_me;
  • type: "withdrawal" — this campaign's share of a payout to the creator (amount_cents, currency, status, arrival_date). Withdrawals cannot be liked and carry no like fields.

Withdrawn (deleted) updates are left out. The feed degrades rather than fails: if a source cannot be read, the endpoint answers what it has, down to [].

The website's own Post Update dialog asks for 50–1,000 characters; the API limit above is the wider one.

Likes ​

A signed-in account can like any visible comment (a reply too) and any update on a campaign that a visitor can open. Each like is one row per account per target; nothing is public about who liked what — only the count is shown.

EndpointPurpose
PUT /comments/:commentId/likeLike a comment. Flag features.comments.
DELETE /comments/:commentId/likeTake the like back. Flag features.comments.
PUT /projects/:fundraiserId/updates/:updateId/likeLike a campaign update.
DELETE /projects/:fundraiserId/updates/:updateId/likeTake the like back.

All four take a session (authenticateToken) and the authenticated limiter (100 a minute per IP and account). They do not require a verified email — a like carries no text and is reversible.

Every answer is the caller's state and the target's count after the write:

json
{ "liked": true, "like_count": 4 }

Idempotent by design. PUT means "make it liked" and DELETE means "make it not liked". Sending either twice lands on the same state and the same count, so a client can safely retry after a dropped response — which is why these are PUT/DELETE on a sub-resource and not a POST toggle.

StatusMeaning
200{ liked, like_count }
400An id in the path is not a UUID (Invalid comment ID / Invalid update ID).
401No session.
403features.comments is off (comment likes only).
404Comment not found / Update not found. One answer for missing, hidden, retracted, withdrawn, on a campaign that is not publicly readable, or — for an update — not on the campaign in the path. An unlike of such a target is 404 too.

An owner previewing an unpublished campaign cannot like on it: nothing there is public yet.

Likes publish no domain event and send no notification.

bash
curl -X PUT -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
  https://api.fundlyhub.org/api/v1/projects/$FUNDRAISER_ID/updates/$UPDATE_ID/like

like_count and liked_by_me on the lists ​

ListFields per item
GET /fundraisers/:fundraiserId/commentslike_count, liked_by_me on every comment
GET /projects/:fundraiserId/updateslike_count, liked_by_me on every type: "update" item
POST /fundraisers/:fundraiserId/comments (the created comment)like_count: 0, liked_by_me: false
  • like_count is an integer, counted at read time.
  • liked_by_me is true only when the request carries a session whose account liked the item. Both lists are public and take optional auth, so a guest — or a request without credentials — always reads false. Send your session or API key on these reads if you want liked_by_me to reflect the user.
  • Treat a missing field as 0 / false; a degraded read of the update feed can leave them out.

The per-account feed GET /me/campaign-updates does not carry the like fields; read them from the campaign's own update list.

Built with VitePress