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.
{ "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.
Removes 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.
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.
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):
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.
Status
Meaning
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.
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.
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.
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.
Status
Meaning
200
{ liked, like_count }
400
An id in the path is not a UUID (Invalid comment ID / Invalid update ID).
401
No session.
403
features.comments is off (comment likes only).
404
Comment 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, 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.
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.
Comments
GET /fundraisers/:fundraiserId/comments?limit=(default 50, max 100),?offset=.POST /fundraisers/:fundraiserId/commentsfeatures.comments{ "content": "…", "parent_comment_id"?: "…", "gif_id"?: "…" }.contentis trimmed and must be 1–2,000 characters, or may be empty whengif_idis set (GIFs). A reply's parent must be on the same campaign.DELETE /comments/:idThe 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, statusactive,endedorpaused, and notprivate. Comments on any other campaign are not readable here, even by id.POSTanswers201with{ data: comment }, already carryinglike_count: 0andliked_by_me: false, so a client can prepend it to the list without a refetch.Every comment row, in the list, in the
POSTanswer and in the admin listings, carriesgif: a GIF object ornull.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.GET /gifs/trending?limit=(default 24, max 50),?offset=.GET /gifs/search?q=(required),?limit=,?offset=,?lang=en|ru|uk|es.Both answer
{ "data": [Gif, …], "next_offset": 24 }. For the next page, passnext_offsetasoffset. It isnullon the last page. Results are ratedpgor lower and cached on the server: trending for five minutes, and searches for ten. Both reads, and a commentPOSTthat carriesgif_id, share a limit of 60 a minute per account (Rate Limits).The GIF object, on every comment row as
gif(ornull):Every URL is
https://on agiphy.comsubdomain, with no query string. The full size is GIPHY'soriginal, the still isoriginal_still, and the preview is the 200 px widefixed_width. Showtitleas text (alt text), never as HTML. Playmp4_urlmuted and looped withstill_urlas the poster; under reduced motion, show the still. Show GIPHY's attribution ("Powered by GIPHY") in your picker.503{ "error": "gifs_unavailable" }features.comment_gifs) or GIPHY is not configured. Hide the GIF button. APOSTwithgif_idgets it too.502{ "error": "gifs_upstream" }400{ "error": "invalid_query" }GET /gifs/searchwithout a usableq.400{ "error": "Invalid gif_id" }gif_idis not a GIPHY id (^[A-Za-z0-9]{6,40}$).422{ "error": "gif_not_found" }422{ "error": "gif_not_allowed" }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
GET /projects/:fundraiserId/updatesPOST /projects/:fundraiserId/updatesfeatures.project_updates{ "title"?: "…", "body": "…" }— body required, up to 20,000 characters; title up to 200. Longer is413.PATCH /projects/:fundraiserId/updates/:updateIdfeatures.project_updatesDELETE /projects/:fundraiserId/updates/:updateIdThe feed is a bare JSON array, not wrapped in
data. It interleaves two kinds of item by date, told apart bytype:type: "update"— an update row withauthor: { id, name, avatar },like_countandliked_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.
PUT /comments/:commentId/likefeatures.comments.DELETE /comments/:commentId/likefeatures.comments.PUT /projects/:fundraiserId/updates/:updateId/likeDELETE /projects/:fundraiserId/updates/:updateId/likeAll 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:
Idempotent by design.
PUTmeans "make it liked" andDELETEmeans "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 arePUT/DELETEon a sub-resource and not aPOSTtoggle.200{ liked, like_count }400Invalid comment ID/Invalid update ID).401403features.commentsis 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 is404too.An owner previewing an unpublished campaign cannot like on it: nothing there is public yet.
Likes publish no domain event and send no notification.
like_countandliked_by_meon the lists GET /fundraisers/:fundraiserId/commentslike_count,liked_by_meon every commentGET /projects/:fundraiserId/updateslike_count,liked_by_meon everytype: "update"itemPOST /fundraisers/:fundraiserId/comments(the created comment)like_count: 0,liked_by_me: falselike_countis an integer, counted at read time.liked_by_meistrueonly 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 readsfalse. Send your session or API key on these reads if you wantliked_by_meto reflect the user.0/false; a degraded read of the update feed can leave them out.The per-account feed
GET /me/campaign-updatesdoes not carry the like fields; read them from the campaign's own update list.Related