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

Plain text
https://api.appwin.io/api/sdk/community/v1

Authentication

Bearer token, obtained by AppwinCore at configure:

Plain text
Authorization: Bearer <token>

The token alone carries the organisation, the project and the member's profile: no tenant identifier travels in the paths.

HeaderRole
AuthorizationRequired
X-Appwin-LanguageThe reader's language (ISO 639-1), for translation
If-None-MatchCached config version, on /config

Status codes

CodeMeaning
401Token missing, invalid or revoked
403Community product disabled on the App ID, community switched off, or member banned
404Resource missing or outside the member's project
400Validation 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.

json
{
  "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

ParameterDefaultDescription
groupIdallRestricts to one group
sortrecentrecent or top
cursorOpaque cursor for the next page
limit201 to 50
json
{ "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

json
{ "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

json
{ "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

json
{ "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:

json
{ "targetId": "…", "myReaction": "like", "likeCount": 13 }

POST /views

json
{ "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

json
{ "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

json
{ "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

json
{ "notificationIds": [] }

An empty list means mark everything as read.

POST /translate

json
{ "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/signPOST /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:

Plain text
wss://api.appwin.io/realtime/sdk/community

Authenticated with the same token, in the handshake's auth.token.

EventTrigger
community.post.createdA post went live
community.post.updatedA post was edited or moderated
community.post.deletedA post was deleted
community.comment.createdA comment was added
community.comment.deletedA comment was deleted
community.reaction.changedA 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.