Comments
Comments are what people write under a post or an event, and the replies to them. These endpoints move the comments of the old platform in, with their original dates and authors, and let you correct or remove what you imported. Read Start here first: its rules apply to everything on this page. Comments work like Posts and Events, and this page says where they differ.
Import the posts and events first. A comment goes on a post or an event that already exists in Zen Social, and a reply
goes on a comment that already exists, so a move runs in this order: posts and events, then comments, parents before
their replies. Keep your own list of the id each external_id was given.
What the API has, and what it does not
- Comments on posts and on events, replies included.
- Not in the API: pictures on comments (the app's comment form has none either) and comments on newsletter
editions. A comment on an edition is not listed, and
GET,PATCHandDELETEanswer404 not_foundfor it.
The comment object
{
"id": 9201,
"external_id": "fb-comment-8001",
"post_id": 9001,
"event_id": null,
"parent_id": null,
"agenda_item_id": null,
"author": {"id": "EveABqLyto9L", "name": "Karen Holm"},
"content": "I can bring basil seedlings on Saturday.",
"hidden": false,
"deleted": false,
"created_at": "2023-04-15T12:30:00.000Z",
"edited_at": null
}
| Field | Type | Meaning |
|---|---|---|
id |
number | The comment's id. Use it in /comments/:id, and as parent_id for a reply. |
external_id |
text or null |
Your id from the old platform, when the comment was created through the API with one. null for every other comment. |
post_id |
number or null |
The post the comment is on. |
event_id |
number or null |
The event the comment is on. Exactly one of post_id and event_id is set. A reply carries the post or event of the whole thread, however deep it is. |
parent_id |
number or null |
The comment this one replies to. null for a comment directly on the post or event. |
agenda_item_id |
number or null |
For a comment on a meeting: the agenda item it was written under, in the app. A reply carries the item of the comment it replies to, also when you create it here. null for a comment in the event's own thread, and for every comment on a post. The API cannot put a comment under an item. |
author |
object or null |
Who wrote it, in the same two shapes as a post's author (below). null for a hidden comment. |
content |
text | The text, as plain text. Empty ("") for a deleted or a hidden comment. |
hidden |
true or false | true when the comment is the app's placeholder for you: see "Hidden authors" below. |
deleted |
true or false | true for a comment that has been deleted: see "Deleted comments" below. |
created_at |
time | When it was written: the original date for an imported comment. |
edited_at |
time or null |
When a person last edited it in the app. Changes through the API never set it. |
author is one of two shapes:
- A member:
{"id": "EveABqLyto9L", "name": "Karen Holm"}. Theidis the member's id from Members. - A name from the old platform, for someone who has no Zen Social account (yet):
{"id": null, "name": "Ole Berg", "external_id": "fb-user-1002"}. Theexternal_idis your own id for that person, as you sent it;nullif the comment was created without anexternal_idof its own. See Authors and claims.
Comments go with the account of their author when it is deleted. The one kept comment is on a meeting's agenda: it shows
"id": null and the name the account had, or "name": null when it had none.
There are no email addresses and no picture addresses.
List them: GET /comments
Comments are listed for one post or one event at a time. Give post_id or event_id, and not both:
curl "$API/comments?post_id=9001" -H "Authorization: Bearer $KEY"
curl "$API/comments?event_id=9101" -H "Authorization: Bearer $KEY"
Answers 200 OK with {"data": [<comment>, …], "next_page": null}, 100 per page (?page=2 for the next). The
newest comment comes first, and comments with the same time by their id, highest first. All the comments of the post or
event come in one list, replies included, not nested: use parent_id to build the thread. A reply to a comment on
another page is on the same list, so read every page before you nest.
A comment is listed exactly as the admin would see it in the app, and nothing more:
404 not_foundwhen the post or event does not exist, is not in one of this community's circles, has been deleted, or is one the admin could not see in the app. (Here it is never403.)- Deleted comments are in the list (below).
- Comments of authors the admin has blocked, or who have blocked the admin, are in the list as placeholders (below).
422 invalid when neither post_id nor event_id is given, or both.
Deleted comments
A deleted comment stays in the list as a tombstone: "deleted": true and "content": "". Its id, parent_id,
dates and author stay, so its replies still have a place in the thread. GET /comments/:id answers 200 OK with the
tombstone too.
Hidden authors
When the admin has blocked the author, or the author has blocked the admin, the app shows the comment as a grey
placeholder, and keeps its replies. The API gives the same placeholder: "hidden": true, "content": "" and
"author": null, with the rest as it is. A comment that is both hidden and deleted also has "author": null.
The placeholder follows the admin who made the key: another admin, with no block, sees the comment itself.
A block does not stop a PATCH or a DELETE: the rules in "Who may change which comment" apply as without it. The
PATCH changes the text, and answers with the placeholder ("content": ""), not with the text you sent.
Read one: GET /comments/:id
curl "$API/comments/9201" -H "Authorization: Bearer $KEY"
Answers 200 OK with the comment, also when it is deleted or hidden. 404 not_found when it is not on a post or event
in one of this community's circles, or is on one that has been deleted or that the admin could not see in the app.
Create one: POST /comments
The first example is a comment on a post under a member. The second is a reply to it, under a name from the old platform:
curl "$API/comments" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id": "fb-comment-8001", "post_id": 9001, "author_id": "EveABqLyto9L", "created_at": "2023-04-15T12:30:00Z", "content": "I can bring basil seedlings on Saturday."}'
curl "$API/comments" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id": "fb-comment-8002", "post_id": 9001, "parent_id": 9201, "author": {"external_id": "fb-user-1002", "name": "Ole Berg"}, "created_at": "2023-04-15T18:05:00Z", "content": "Saturday is fine. I will keep the table free."}'
A comment on an event takes event_id instead of post_id:
curl "$API/comments" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id": "fb-comment-8003", "event_id": 9101, "author_id": "EveABqLyto9L", "created_at": "2026-10-21T09:00:00Z", "content": "Is there parking near the garden hut?"}'
| Field | Rules |
|---|---|
post_id or event_id |
Required: exactly one of them, the id of the post or event the comment goes on. It must be in a circle of this community that the admin can see, and the admin must be able to comment in every circle it is in: 403 forbidden otherwise. 404 not_found for one that does not exist, is in no circle of this community, is deleted, or that the admin cannot see. |
content |
Required: plain text, not empty. The same rules as a post's text: see The text. Comments have no word limit. |
parent_id |
The id of the comment this one replies to. Optional: without it the comment goes directly on the post or event. See "Replies". |
external_id |
Your id for this comment on the old platform, as text or a number, at most 200 characters. Always send it: re-runs, changing the comment later and handing it back to its author all depend on it (see "Who may change which comment"). |
author_id |
A member's id from Members. They must be a member of every circle the post or event is in. |
author |
{"external_id": "…", "name": "…"} for someone who has not joined. The same rules as for a post: see Who wrote it. |
created_at |
The original date and time, ISO 8601, in the past and no earlier than 1 January 1990. The same rules as for a post: see The date. Default: now. |
notify |
true to tell people about the comment the way the app would. Default false. See "Telling people". |
With neither author_id nor author, the admin who made the key is the author. Fields the API does not know are
ignored without an error, except images and image_urls: comments take no pictures, so those are refused (below).
Answers:
201 Createdwith the new comment.200 OK,409 record_deleted,409 changed_since_import,409 conflictor403 forbiddenwhen theexternal_idwas imported before: see "Re-runs" below.403 forbidden: the admin cannot comment in one of the circles of the post or event.404 not_found: the post or event is not found (above), also when it has been deleted.422 invalid: a value is not allowed;detailsnames each field and why, for example{"content": ["is required"]}. Nothing was made. This includesimagesandimage_urls({"images": ["comments take no images"]}), and aparent_idthat cannot be used (below).422 reply_to_another_thread,422 reply_too_deep,422 reply_to_deletedand422 not_accepting_comments: see "Replies". Nothing was made.422 notify_needs_member_author: see "Telling people". Nothing was made.
The checks run in this order, and the first that fails answers: 422 for a malformed external_id, then a re-run if
the external_id is known (it answers as in "Re-runs", and the checks below are not made), then 422 for a missing,
doubled or malformed post_id or event_id, 404 for the post or event, 422 not_accepting_comments (a cancelled
event), 403 for a circle the admin cannot comment in, then the other 422 answers. On a cancelled event, an admin who also cannot comment gets the 422,
not the 403.
Replies
Send parent_id to make a reply. The reply goes in the thread of its parent and shows under it. The rules are those
of the app's Reply link:
- The parent must be on the same post or event. A parent on another one is
422 {"error":"reply_to_another_thread"}. - The parent must not be deleted:
422 {"error":"reply_to_deleted"}. A reply to a comment that has replies of its own is fine. - Replies nest at most 9 levels deep. A comment directly on the post or event is level 0, its reply level 1, and so
on down to level 9. A reply to a comment that is already at level 9 is
422 {"error":"reply_too_deep"}. If the old platform allows deeper threads, attach the deepest replies to the last comment that can take them, and tell the admin. - The post or event must still take comments: a cancelled event does not, and the answer is
422 {"error":"not_accepting_comments"}. A comment that is not a reply gets the same answer. A deleted post or event is not found at all:404 not_found. - A
parent_idthat is not an id of a comment, or is one the admin cannot read (it does not exist, it is in another community, it is on a post or event the admin cannot see), answers422 invalidwithdetails.parent_id. The two causes give exactly the same answer on purpose: the API does not tell you which ids exist elsewhere.
You give the post or event as well as the parent. The parent has to be a comment on that same post or event.
No pictures
images and image_urls are refused with 422 invalid, under the field that was sent. They are not ignored, so a
comment is never moved without its picture by accident. If you decide to move such a comment without the picture, leave
the two fields out, and tell the admin which comments lost a picture.
Telling people: notify
By default nothing tells anyone about a comment you create (see Silent by default).
"notify": true makes the create announce the comment exactly as the app does when a member comments: notifications
and push messages, and lines in daily summaries, each according to people's own notification settings. Send it only
when the admin has asked for people to be told, and never for imported history. It announces the comment once, when it
is created; a re-run never announces it again.
A comment with notify needs a real member as its author (author_id, or neither field): a name from the old platform
cannot speak to anyone. With author the answer is 422 {"error":"notify_needs_member_author"}. Its author also needs
a complete profile, as in the app (422 invalid, with the reason under details.base).
Re-runs
Send a create again with the same external_id, and the comment is not made twice. The answers are those of
Re-runs and external_id, the same as for posts and events:
| Answer | Meaning |
|---|---|
200 OK |
The comment was updated and keeps its id. |
409 record_deleted |
The comment was deleted (in the app or through the API), or the post or event it is on was deleted. It is never brought back. Skip it and report it. |
409 changed_since_import |
Someone edited the comment in the app since your last write. It was not overwritten. Skip it and report it. A reply to it, or a claim, does not count as an edit. |
409 conflict |
The same create arrived twice at once. Send it again, once. |
403 forbidden |
The admin can no longer comment in one of the circles of the post or event. Skip it and report it. |
422 invalid |
The content sent is not allowed, or images or image_urls were sent (see "Create one"); details says why. Nothing was changed. |
There is no 409 in_edition for comments: a newsletter edition does not lock a comment.
A re-run changes only content, and only when it is sent. The post or event, the parent, the author, created_at
and notify are ignored: a re-run does not even check them, and never tells anyone. To change any of those, delete the
comment (with the admin's yes) and create it under a new external_id.
An edit in the app counts, even a quiet one. When a person opens an imported comment in the editor and saves it
without changing a word, that is an edit too: pressing Save is what counts, not whether the text changed. The next
re-run answers 409 changed_since_import for it, and the comment stays as the person left it.
A post that was turned into an event in the app keeps its comments. A re-run of one of its comments answers
200 OK, and the comment now has event_id set and post_id null. A re-run ignores the post_id or event_id you
send.
Change one: PATCH /comments/:id
Send content, with the same rules as for a create. Nothing else can be changed: other fields are ignored. Without
content the answer is 200 OK and nothing changes.
curl -X PATCH "$API/comments/9201" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"content": "I can bring basil and chive seedlings on Saturday."}'
Before a PATCH of a comment whose author is now a member (its author has an id), or whose edited_at is
not null, show the admin the change (the comment as it is and as it will be) and wait for their yes. Those may be a
person's own words, written or corrected by them.
Answers 200 OK with the comment, 422 invalid with the reasons (an empty content, or images; and, for a comment
not created through the API with an external_id, an author who has not completed their profile in the app, under
details.base), 403 forbidden when the admin may not change this comment (see "Who may change which comment"), or
404 not_found. A deleted comment answers 404 not_found: it cannot be changed, and it is not brought back.
A change through the API is not an edit by a person: the comment keeps its date and shows no "(Edited)" mark, and nobody
is told. A later re-run of its create still updates it. A comment someone edited in the app is different: it keeps
that mark after your PATCH, so a later re-run still answers 409 changed_since_import.
Delete one: DELETE /comments/:id
Warning: this deletes the comment for everyone. It is replaced by a deleted comment in the thread. There is no undo, and a re-run never brings it back.
Never send a DELETE without the admin's clear yes. Tell them which comment it is (its author, date and the start of its text) and wait for their answer. Only this comment is deleted: its replies stay, under the deleted comment, as in the app.
curl -X DELETE "$API/comments/9201" -H "Authorization: Bearer $KEY"
Answers 204 No Content. 403 forbidden when the admin may not delete it, 404 not_found when it is not this
community's. A second DELETE of the same comment answers 404 not_found. A later create with its external_id
answers 409 record_deleted. Nobody is told.
Who may change which comment
Comments you imported: a comment created through this API with an external_id may be re-run, changed,
deleted and handed to its author through the API, whoever its author is now, as long as the admin can still comment in
every circle of its post or event. That holds for any admin's key in this community, and after a claim has made a
member its author. This is the steward right: whoever moves the content in can still correct it.
Every other comment follows the app's rules, as if the admin did it in the app: only its author can change it
(the admin's own comments, and comments created through the API without an external_id and without an author), and
its author or an owner or admin of one of the circles of its post or event can delete it. A comment created without an
external_id under another member's author_id or under a name from the old platform has no record in the API.
You cannot PATCH it afterwards (403 forbidden), and you can delete it only with the rights the app gives: as its
author, or as an owner or admin of one of the circles. A comment under a name can never be claimed either. Send an
external_id for every comment you may want to change later.
The steward right exists only in this API. In the app, an imported comment follows the usual rules: a member author can edit it like any comment of theirs, and a comment under a name from the old platform cannot be edited by anyone there.
Nobody is told
Creating, re-running, changing, deleting and claiming comments tells no member anything, unless a create sends
"notify": true. Imported comments are not announced later either, and the post or event they are on does not move up
in the circle's activity: a comment you add to an old post leaves the post where it was in the feed.