Posts
Posts are what people write in a circle: text, with a link or pictures. These endpoints move the posts 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.
The post object
{
"id": 9001,
"external_id": "fb-post-5001",
"circle_ids": [412],
"author": {"id": "EveABqLyto9L", "name": "Karen Holm"},
"content": "Who has spare tomato seedlings?\n\nI can swap basil.",
"link_url": null,
"images": [{"id": 3307, "filename": "seedlings.jpg"}],
"created_at": "2023-04-15T10:00:00.000Z",
"edited_at": null
}
| Field | Type | Meaning |
|---|---|---|
id |
number | The post's id. Use it in /posts/:id. |
external_id |
text or null |
Your id from the old platform, when the post was created through the API with one. null for every other post. |
circle_ids |
list of numbers | The circles it is in. Only this community's circles are listed. |
author |
object | Who wrote it (below). |
content |
text | The text, as plain text. |
link_url |
text or null |
The address of its link preview. null until Zen Social has fetched the preview, and stays null if the preview could not be fetched. |
images |
list | Its pictures, as {"id", "filename"}. There is no picture address: the pictures belong to the circle's members, and the API does not hand them out. |
created_at |
time | When it was written: the original date for an imported post. |
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 post was created without anexternal_idof its own. See Authors and claims.
There are no email addresses, and no picture or attachment addresses: posts are not an export.
List them: GET /posts
curl "$API/posts" -H "Authorization: Bearer $KEY"
Answers 200 OK with {"data": [<post>, …], "next_page": null}, newest first (by created_at), 100 per page
(?page=2 for the next).
The list shows exactly what the admin would see in the app, and nothing more:
- only posts in circles the admin is a member of, including the community's own feed;
- in a curated circle, a plain member sees only their own posts and those published in an edition, and so does the admin unless they are that circle's owner, admin or editor;
- posts by people the admin has blocked, or who have blocked the admin, are left out;
- deleted posts are left out.
One circle only: add ?circle_id=. The community's own feed is a circle here too: its id is the id from
GET /organization.
curl "$API/posts?circle_id=412" -H "Authorization: Bearer $KEY"
404 not_found when that circle is not one of this community's; 403 forbidden when the admin is not a member of it.
Read one: GET /posts/:id
curl "$API/posts/9001" -H "Authorization: Bearer $KEY"
Answers 200 OK with the post. 404 not_found when it is not in one of this community's circles, or has been
deleted; 403 forbidden when the admin could not see it in the app.
Create one: POST /posts
curl "$API/posts" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id": "fb-post-5001", "circle_ids": [412], "author_id": "EveABqLyto9L", "created_at": "2023-04-15T10:00:00Z", "content": "Who has spare tomato seedlings?\n\nI can swap basil."}'
| Field | Rules |
|---|---|
circle_ids |
Required: a list of one or more circle ids of this community. The post appears in each of them. The admin must be able to post in every one (not just read it): 403 forbidden otherwise, and 404 not_found for a circle outside the community. |
content |
Required: plain text (below). |
external_id |
Your id for this post on the old platform, as text or a number, at most 200 characters. Always send it: re-runs depend on it, and so does handing the post back to its author later (see Authors and claims). An empty string counts as none. |
author_id |
A member's id from Members. See "Who wrote it". |
author |
{"external_id": "…", "name": "…"} for someone who has not joined. See "Who wrote it". |
created_at |
The original date and time, ISO 8601 (below). Default: now. |
link_url |
An address for a link preview (below). |
image_urls |
A list of picture addresses for Zen Social to fetch (below). |
images |
Picture files, in a multipart form (below). |
notify |
true to tell people about the post the way the app would. Default false. See "Telling people". |
Fields the API does not know are ignored without an error.
Answers:
201 Createdwith the new post.200 OK,409 record_deleted,409 changed_since_import,409 in_edition,409 conflictor403 forbiddenwhen theexternal_idwas imported before: see "Re-runs" below.403 forbidden: the admin cannot post in one of the circles.404 not_found: one of the circles is not this community's.422 invalid: a value is not allowed;detailsnames each field and why, for example{"author_id": ["must be the id of a member of every circle the post goes into"]}. Nothing was made.422 notify_needs_member_author:"notify": truewith anauthorname. Nothing was made.
The text: content
Plain text, not HTML and not Markdown: what you send is shown exactly as written.
- A blank line starts a new paragraph. A single line break stays a line break. In JSON, write them as
\n\nand\n. - Addresses starting with
http://orhttps://become links by themselves. <b>,**bold**and the like are shown as typed, not as formatting.
It cannot be empty. The answer gives it back as plain text.
A circle can have a word limit for posts. Imports do not have to keep to it, since they were written elsewhere; a post
sent with "notify": true does, as in the app (422, with the reason under details.content).
Who wrote it: author_id or author
Send one of these, or neither:
author_id: the person is already a member. Use theiridfromGET /members. They must be a member of every circle incircle_ids, or the answer is422withdetails.author_id. The post is theirs, as if they had written it in the app.author: the person has not joined (yet). Send{"external_id": "fb-user-1002", "name": "Ole Berg"}:external_idis your own id for that person on the old platform, as text or a number, 1 to 200 characters. A number comes back as text (1002as"1002"), and that text is the id in a claim's address. Use the same one for every post by the same person: it is how all their posts are handed to them later, in one go.nameis the name the post shows, 1 to 100 characters, after spaces at either end are trimmed.
The post shows that name, with no profile and no link. When the person joins, you can hand the post to them: see Authors and claims. - Neither: the admin who made the key is the author.
Both at once is 422 with details.author.
The date: created_at
The post's original date and time, in ISO 8601, for example 2023-04-15T10:00:00Z or 2023-04-15T12:00:00+02:00.
Send the offset (or Z for UTC): a time without one is read as UTC. It must be in the past, and no earlier than
1 January 1990; otherwise the answer is 422 with details.created_at.
The post keeps that date everywhere: it sorts by it in the feeds, it does not count as new on anyone's Home page, and
its circle does not look "active just now". Without created_at the post is dated now.
A link: link_url
An absolute http or https address, at most 2000 characters. The post shows a preview of it (title, picture,
summary), as when a member pastes a link in the app. Zen Social fetches the preview shortly after the post is saved:
until then, and when it cannot be fetched, the answer shows "link_url": null.
Pictures: images and image_urls
At most 10 pictures a post, counted together. JPEG, PNG or AVIF only, judged by the file's contents, not by its name or the type a server declares for it.
image_urls, in a JSON body: a list of publichttporhttpsaddresses. Zen Social fetches each one while you wait, at most 5 MB each. That is slow: send posts with many pictures one at a time.
curl "$API/posts" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"external_id": "fb-post-5003", "circle_ids": [412], "author": {"external_id": "fb-user-1002", "name": "Ole Berg"}, "created_at": "2023-05-02T18:30:00Z", "content": "The swap table is up!", "image_urls": ["https://example.org/swap-table.jpg"]}'
images, files in a multipart form, at most 10 MB each. The other fields go in the same form: a list ascircle_ids[], the author name asauthor[external_id]andauthor[name].
curl "$API/posts" \
-H "Authorization: Bearer $KEY" \
-F "external_id=fb-post-5004" -F "circle_ids[]=412" -F "author_id=EveABqLyto9L" \
-F "created_at=2023-05-03T09:00:00Z" -F "content=Photos from Saturday" \
-F "images[]=@saturday-1.jpg" -F "images[]=@saturday-2.jpg"
A picture that cannot be used answers 422 and nothing is saved: an address that cannot be fetched or is not a
picture under details.image_urls, a file of the wrong kind or too large under details.attachments, more than 10
under details.images.
The pictures are private to the members of the post's circles, as pictures posted in the app are.
Telling people: notify
By default nothing tells anyone about a post you create (see Silent by default).
"notify": true makes the create announce the post exactly as the app does when a member posts: notifications, push
messages and lines in daily summaries, for the circle's members, each according to their own notification settings.
Send it only when the admin has asked for people to be told, and never for imported history.
A post 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"}. A post with notify
also keeps to the circle's word limit, and its author needs a complete profile, as in the app (422 invalid
otherwise).
Re-runs
Send a create again with the same external_id, and the post is not made twice. The answers are those of
Re-runs and external_id:
| Answer | Meaning |
|---|---|
200 OK |
The post was updated and keeps its id. |
409 record_deleted |
The post was deleted (in the app or through the API), or turned into an event in the app. It is never brought back. Skip it and report it. |
409 changed_since_import |
Someone edited the post in the app since your last write. It was not overwritten. Skip it and report it. A comment on the post, or a claim, does not count as an edit. |
409 in_edition |
The post is in a newsletter edition, so its text can no longer change. Nothing was changed. Skip it and report it. |
409 conflict |
The same create arrived twice at once. Send it again, once. |
403 forbidden |
The admin can no longer post in one of the post's circles. Skip it and report it. |
422 invalid |
The content or link_url sent is not allowed (see "Create one"); details says why. Nothing was changed. |
A re-run changes only content and link_url, and only when they are sent. Everything else about the post stays as
it was first imported, and a re-run does not even check it: circle_ids, the author, created_at, pictures and
notify are ignored. A re-run never moves a post, never changes its date or author, never adds pictures and never
tells anyone. To change any of those, delete the post (with the admin's yes) and create it under a new external_id.
Change one: PATCH /posts/:id
Send content, link_url or both, with the same rules as for a create. "link_url": null removes the link preview.
Nothing else can be changed: other fields are ignored.
curl -X PATCH "$API/posts/9001" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"content": "Who has spare tomato seedlings?\n\nI can swap basil or chives."}'
Before a PATCH of a post whose author is now a member (its author has an id), or whose edited_at is not
null, show the admin the change (the post 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 post, 422 invalid with the reasons, 403 forbidden when the admin may not change this post
(see "Who may change which post"), 409 in_edition when the post is in a newsletter edition, or 404 not_found.
A change through the API is not an edit by a person: the post shows no "edited" mark, keeps its date, and nobody is
told. A later re-run of its create still updates it. A post 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.
A post created through the API with an external_id need not keep to its circles' word limit when you change it;
any other post must, as in the app.
Delete one: DELETE /posts/:id
Warning: this deletes the post for everyone. Its comments go with it and its pictures are removed. 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 post it is (its author, date and the start of its text) and wait for their answer.
curl -X DELETE "$API/posts/9001" -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 or is already deleted. A later create with its external_id answers 409 record_deleted.
Who may change which post
Posts you imported: a post 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 post in every one of
its circles. 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.
A post in a newsletter edition can no longer be changed, not even under the steward right: the edition shows it as
published, and in the app not even its author can edit it. A PATCH or a re-run answers
409 {"error":"in_edition"} and changes nothing. It can still be deleted.
Every other post follows the app's rules, as if the admin did it in the app: only its author can change it (the
admin's own posts, and posts created through the API without an external_id and without an author), and not even its
author once it is in a newsletter edition (403 forbidden); its author or an owner or admin of one of its circles can
delete it. A post created without an external_id under someone else's
name or member id cannot be changed through the API afterwards: another reason to always send one.
The steward right exists only in this API. In the app, an imported post follows the usual rules: a member author can edit it like any post of theirs; a post under a name from the old platform cannot be edited by anyone in the app, and an owner or admin of its circle can delete it.
Nobody is told
Creating, changing and deleting posts tells no member anything, unless a create sends "notify": true.
Two things to know about communities whose circles share posts with other servers on the open social web (federation): deleting a post through the API also asks those servers to remove their copy, and changing a post through the API does not update the copies they already have.