Reference - Community SDK API
The endpoints the SDK consumes. Documented for debugging, and for a possible port to a platform we do not cover.
You do not have to call them yourself if you use the SDK: it handles them, session and token renewal included.
Base
https://api.appwin.io/api/sdk/community/v1
Authentication
Bearer token, obtained by AppwinCore at configure:
Authorization: Bearer <token>
The token alone carries the organisation, the project and the member's profile: no tenant identifier travels in the paths.
| Header | Role |
|---|---|
Authorization | Required |
X-Appwin-Language | The reader's language (ISO 639-1), for translation |
If-None-Match | Cached config version, on /config |
Status codes
| Code | Meaning |
|---|---|
401 | Token missing, invalid or revoked |
403 | Community product disabled on the App ID, community switched off, or member banned |
404 | Resource missing or outside the member's project |
400 | Validation refused: length, flood protection, content rejected by moderation |
Startup
GET /bootstrap
Config, groups, profile and badge in one round trip. Counts as one open of the community in the statistics.
{
"config": { "theme": {}, "features": {}, "limits": {}, "context": {}, "version": 3 },
"groups": [
{ "id": "…", "name": "General", "emoji": "💬", "isDefault": true, "canPost": true, "postCount": 42 }
],
"profile": { "id": "…", "nickname": "Curious Otter 417", "isAnonymous": true, "isMe": true },
"unreadNotificationCount": 3
}
GET /config
Config on its own. Send If-None-Match: <version> to get a 304 when nothing
has changed.
The moderation policy is never served: a member has no business reading the threshold they would have to get around.
Feed
GET /feed
| Parameter | Default | Description |
|---|---|---|
groupId | all | Restricts to one group |
sort | recent | recent or top |
cursor | — | Opaque cursor for the next page |
limit | 20 | 1 to 50 |
{ "data": [ { "id": "…", "body": "…", "likeCount": 12 } ], "nextCursor": "eyJ…" }
The cursor is opaque: send it back as is, never interpret it.
nextCursor: null signals the end.
What the feed serves: published posts, plus the member's own awaiting review. Content from shadow banned members is absent for everyone except its author.
POST /posts
{ "groupId": "…", "body": "…", "media": [ { "url": "…", "width": 1200, "height": 800 } ] }
groupId omitted → default group. Rejected if the length exceeds the project's
limit, if the rate exceeds flood protection, or if moderation blocks it.
PATCH /posts/:id · DELETE /posts/:id
Reserved to the author. An edit goes back through moderation: without that, posting innocuous text then editing it would be enough to bypass the filter.
Comments
GET /posts/:id/comments
Root comments, each with its first replies inline. replyCount gives the server
total, which can exceed the replies delivered.
POST /posts/:id/comments
{ "body": "…", "parentCommentId": "…" }
One level only: replying to a reply attaches to the same root parent, without the client having to know.
DELETE /comments/:id
Reactions and views
POST /posts/:id/reactions · POST /comments/:id/reactions
{ "kind": "like" }
Three behaviours depending on state: set, replace, or remove if the same one is set again. The response lets you update the counter without a refetch:
{ "targetId": "…", "myReaction": "like", "likeCount": 13 }
POST /views
{ "postIds": ["…", "…"] }
A batch of seen posts, sent when the member leaves the screen. Views are unique per (post, member): scrolling back inflates nothing.
Profile
GET /profiles/:id
POST /me - the setUser bridge
{ "nickname": "…", "avatarUrl": "…", "bio": "…" }
Optional fields; an absent field is not overwritten. Providing a nickname
takes the profile out of anonymity.
PATCH /me - edited by the member
Also accepts isAnonymous. Setting it back to true regenerates the anonymous
nickname and clears the avatar: anonymity has to be visible, not just a flag.
Reporting, notifications, translation
POST /reports
{ "targetType": "post", "targetId": "…", "reason": "spam", "note": "…" }
targetType: post, comment, profile.
reason: spam, harassment, hate_speech, sexual_content, violence,
misinformation, off_topic, other.
Always 204, even if that member had already reported that target. Telling
them "already reported" does not help them and reveals the state of the queue.
GET /notifications · POST /notifications/read
{ "notificationIds": [] }
An empty list means mark everything as read.
POST /translate
{ "targetType": "post", "targetId": "…", "targetLanguage": "fr" }
targetLanguage omitted → X-Appwin-Language. The result is cached: the first
reader of a language pays for the call, the following ones read the database.
Uploads
POST /uploads/sign → POST /uploads/:id/confirm
Signing a direct upload to storage, then confirming it. The purpose is forced
server-side to community_media: a client cannot target another use even by
altering its request.
Realtime
Socket.IO namespace:
wss://api.appwin.io/realtime/sdk/community
Authenticated with the same token, in the handshake's auth.token.
| Event | Trigger |
|---|---|
community.post.created | A post went live |
community.post.updated | A post was edited or moderated |
community.post.deleted | A post was deleted |
community.comment.created | A comment was added |
community.comment.deleted | A comment was deleted |
community.reaction.changed | A reaction was set or removed |
The payload is minimal (resourceId, projectId): the client makes a REST
call to fetch the resource.
That is not bandwidth thrift, it is a matter of correctness. A room gathers every member of a project; pushing content into it would mean replaying every visibility rule on the socket side - shadow ban, moderation, content awaiting review - and one day one of them would be missing. The REST refetch goes through those rules once, in the right place.