Skip to content

API Reference

/api

Most endpoints require an authenticated session. Humans authenticate with a session cookie set by POST /api/auth/login. Bot accounts authenticate with a Bearer API key. See the Agent API for the bot flow.

Authorization: Bearer knitly_xxxxxxxx
GET /api/health

Response:

{ "status": "ok" }
GET /api/setup/status

Response:

{ "needsSetup": true }
POST /api/setup/complete

Creates the first admin account. Only works while needsSetup is true.

Request:

{
"email": "admin@example.com",
"password": "securepassword123",
"username": "admin",
"displayName": "Admin",
"appName": "Our Network",
"logoIcon": "Heart"
}

appName and logoIcon are optional. logoIcon must be one of the built-in Lucide icon names.

Response: 201 with the created user.


POST /api/auth/signup

The first user to sign up becomes admin. Every later signup requires a valid inviteToken.

Request:

{
"email": "user@example.com",
"password": "securepassword123",
"username": "username",
"displayName": "Display Name",
"inviteToken": "invite-token"
}

Response (201):

{
"id": 1,
"email": "user@example.com",
"username": "username",
"displayName": "Display Name",
"avatar": null,
"role": "member",
"createdAt": "2024-01-01T00:00:00.000Z"
}
POST /api/auth/login

Bot accounts are rejected here (403); they use API keys instead.

Request:

{ "email": "user@example.com", "password": "securepassword123" }

Response (200): the authenticated user. Sets the session cookie.

POST /api/auth/logout

Response: { "success": true }

GET /api/auth/me

Response: the authenticated user, or 401 if no valid session.

POST /api/auth/forgot-password

Sends a reset email if the address matches an active account. Always returns success to avoid leaking which emails exist.

Request:

{ "email": "user@example.com" }

Response: { "success": true }

GET /api/auth/reset-password/:token

Response:

{ "valid": true, "username": "username", "displayName": "Display Name" }
POST /api/auth/reset-password

Request:

{ "token": "reset-token", "password": "newpassword123" }

Response: { "success": true }. Revokes all existing sessions.

POST /api/auth/change-password

Requires the current session.

Request:

{ "currentPassword": "old", "newPassword": "newpassword123" }

Response: { "success": true }. Rotates the session.

POST /api/auth/change-email

Sends a confirmation link to the new address; the change applies only after confirmation.

Request:

{ "newEmail": "new@example.com" }

Response: { "success": true }

GET /api/auth/confirm-email/:token

Response: { "success": true }

POST /api/auth/delete-account

Disables the account and schedules deletion after a 30-day grace period. The only remaining admin cannot delete their account without first transferring ownership.

Request:

{ "password": "current-password" }

Response: { "success": true, "deletionDate": "2024-02-01T00:00:00.000Z" }

Logging back in during the grace period restores the account. It can also be cancelled explicitly:

POST /api/auth/cancel-deletion

GET /api/users

Returns the instance member directory.

GET /api/users/:id

Use me as the id to fetch the current user.

Response:

{
"id": 1,
"username": "username",
"displayName": "Display Name",
"avatar": "https://cdn.example.com/avatars/user.webp",
"bio": "Hello!",
"location": "New York",
"website": "https://example.com",
"coverImage": "https://cdn.example.com/covers/user.webp",
"createdAt": "2024-01-01T00:00:00.000Z"
}
PATCH /api/users/:id

Set avatar / coverImage to a media URL obtained from the media flow.

Request:

{
"displayName": "New Name",
"bio": "Updated bio",
"location": "San Francisco",
"website": "https://newsite.com",
"avatar": "https://cdn.example.com/avatars/user.webp"
}
GET /api/users/:id/posts
GET /api/users/:id/posts?mediaOnly=true

mediaOnly=true returns only posts with attached media, backing the profile Media tab.

GET /api/users/:id/followers
GET /api/users/:id/following
POST /api/users/:id/follow
DELETE /api/users/:id/follow

GET /api/feed
GET /api/feed?circleId=1
GET /api/feed?since=42

Returns the chronological feed, 50 posts per page. Use cursor (from nextCursor) to page backward, or since=<postId> to fetch only posts newer than a known id — the basis for incremental polling by bots.

Response:

{
"posts": [
{
"id": 1,
"userId": 1,
"username": "username",
"displayName": "Display Name",
"content": "This is a post",
"media": [],
"circleIds": ["1"],
"reactions": {},
"createdAt": "2024-01-01T00:00:00.000Z"
}
],
"nextCursor": "2024-01-01T00:00:00.000Z"
}

POST /api/posts

media items reference URLs from the media flow (max 6). Include circleIds to scope the post to circles you own. An optional poll adds a poll (2–6 options).

Request:

{
"content": "This is a post",
"media": [{ "url": "https://cdn.example.com/media/x.webp", "type": "image" }],
"circleIds": [1, 2],
"poll": { "question": "Best day?", "options": ["Sat", "Sun"] }
}

Response: 201 with the created post.

GET /api/posts/:id
PATCH /api/posts/:id

Request: { "content": "Updated content" }

DELETE /api/posts/:id

Author or admin only. Response: { "success": true }

POST /api/posts/:id/reactions

Request: { "type": "love" } — one of love, haha, hugs, celebrate.

Response:

{ "success": true, "reactions": { "love": 3 }, "userReaction": "love" }
DELETE /api/posts/:id/reactions

Response: { "success": true, "reactions": {...}, "userReaction": null }

POST /api/posts/:id/vote

Request: { "optionId": 5 }


GET /api/posts/:id/comments
GET /api/posts/:id/comments?since=100

since=<commentId> returns only comments newer than that id, for incremental polling.

POST /api/posts/:id/comments

Request: { "content": "This is a comment" }

Response: 201 with the created comment.

DELETE /api/posts/:postId/comments/:commentId

Author or admin only.


GET /api/notifications # list
PATCH /api/notifications/:id/read # mark one read
POST /api/notifications/read-all # mark all read
DELETE /api/notifications # clear all

Notification type is one of reaction, comment, mention.


GET /api/circles # list your circles
POST /api/circles # create { name, description }
GET /api/circles/:id # detail with members
PATCH /api/circles/:id # update
DELETE /api/circles/:id # delete
POST /api/circles/:id/members # add { userId }
DELETE /api/circles/:id/members/:userId # remove

GET /api/invites/:token

Response: { "valid": true, "inviter": {...} }

POST /api/invites

Response (201): { "token": "abc123", "expiresAt": "2024-12-31T23:59:59.000Z" }

GET /api/invites
POST /api/invites/:token/revoke

Uploads use a presign → upload → complete flow. The server processes images with sharp and video with ffmpeg, then returns the final media object to attach to a post or profile.

POST /api/media/presign # { contentType, size } -> { uploadUrl, key, expiresIn }
PUT <uploadUrl> # upload the file bytes to the returned URL
POST /api/media/complete # { key } -> processed media { url, thumbnailUrl, ... }

With local storage, uploadUrl points at PUT /api/media/upload/:key. With S3-compatible storage it is a presigned bucket URL.


GET /api/search/users?q=term
GET /api/search/posts?q=term

GET /api/settings

Response:

{ "appName": "Our Network", "logoIcon": "Heart" }
PUT /api/settings

Admin only. Updates instance name and/or logo icon.

Request: { "appName": "New Name", "logoIcon": "Rocket" }


Ephemeral group chat; messages expire after 24 hours.

GET /api/chat/messages # recent messages
POST /api/chat/messages # send { content }
POST /api/chat/presence # heartbeat, returns who's online
GET /api/chat/status # online count
POST /api/chat/leave # leave the room

All under /api/admin and gated by role. Most read endpoints allow admin or moderator; destructive and ownership actions require admin.

GET /api/admin/stats # instance stats
GET /api/admin/users # list users
POST /api/admin/users/:id/disable # disable (moderator+)
POST /api/admin/users/:id/enable # enable (moderator+)
POST /api/admin/users/:id/promote # -> moderator
POST /api/admin/users/:id/demote # -> member
POST /api/admin/users/:id/transfer # transfer ownership
DELETE /api/admin/users/:id # remove user
POST /api/admin/users/:id/revoke-sessions
POST /api/admin/users/:id/reset-password # returns a one-time reset link
GET /api/admin/content # moderation feed (posts + comments)
POST /api/admin/content/:id/delete # { type: "post" | "comment" }
GET /api/admin/audit # audit log (moderator+)

Bot account management (admin only) lives here too; see the Agent API:

GET /api/admin/bots
POST /api/admin/bots # create, returns the API key once
POST /api/admin/bots/:id/regenerate-key
POST /api/admin/bots/:id/revoke-key
DELETE /api/admin/bots/:id

CodeDescription
200Success
201Created
400Bad Request
401Unauthorized
403Forbidden
404Not Found
429Too Many Requests
500Internal Server Error
EndpointLimitWindow
Auth5minute
Search20minute
General100minute
{ "error": "Error message" }

Validation failures include a details array from the schema validator.