This reference documents the non-administrative HTTP API of FediSuite 2.0 (repository FediSuite-Docker-Image, routes under server/routes).
Not included:
- all
/api/admin/...endpoints - the catch-all
GET *, which only delivers the web app
Important:
- this file describes all endpoints that are statically visible in the core repository, without the admin area
- in addition, active plugins can register their own HTTP routes (see section 11)
- these plugin routes cannot be derived from the core repository because they are mounted from plugin code at runtime
Basic rules
Base URL
All paths are relative to a FediSuite instance, for example:
https://fedisuite.example.comhttps://app.fedisuite.com
The examples use https://fedisuite.example.com.
Authentication
Authenticated requests use:
Authorization: Bearer <JWT>
The JWT is obtained via POST /api/auth/login. If the account has two-factor authentication, the login returns a challenge first and the JWT follows only after POST /api/auth/2fa/verify (section 3).
Properties of the token:
- the JWT is signed with HS256 and carries the user ID and the ID of a session
- it has no expiry date but is bound to a session that is checked on every request
- a session can be revoked via
DELETE /api/user/sessions/:sessionId, after which the API answers with401 - challenge tokens from the login with 2FA are not session tokens and are rejected on protected endpoints
The responses of the authentication middleware have no JSON body, only the status code:
401no token, a revoked session or a token without session data403token with an invalid signature or an unreadable format
Language
Error texts are localized using the Accept-Language header. de, en, es, fr and it are supported, everything else falls back to English.
Accept-Language: de
Time format
Timestamps are usually delivered as ISO-8601 strings, mostly in UTC.
Standard error format
Most often:
{
"error": "Error message"
}
Typical status codes:
400invalid request401missing or invalid token (without a JSON body)403access denied, for example a wrong password404resource not found or feature switched off on this instance409name already taken (labels and campaigns)429rate limit500internal error
Responses under /api are not cached (Cache-Control: no-store). CORS is open, authentication works exclusively through the bearer header.
Rate limits
Some endpoints have a limit per IP address. When it is exceeded the API answers with 429 and { "error": "..." }.
POST /api/auth/login: 10 attempts in 15 minutesPOST /api/auth/register,POST /api/mobile/auth/register,POST /api/auth/resend-verificationandPOST /api/mobile/auth/resend-verification: 5 requests per hour, shared between themPOST /api/auth/forgot-password: 5 requests in 15 minutesPOST /api/auth/reset-password: 10 requests in 15 minutesPOST /api/auth/2fa/verify: 15 attempts in 15 minutesPOST /api/auth/2fa/email/request: 5 requests in 15 minutes- managing two-factor authentication (
/api/auth/2fa/totp/...,/api/auth/2fa/recovery/regenerate,/api/auth/2fa/email/enable,/api/auth/2fa/email/disable): 20 requests in 15 minutes, shared between them
Feature switches
Operators can switch off larger features with an environment variable. All of them are on by default. The routes of a switched-off feature answer with 404, and the current switches are listed under features in GET /api/public/config.
ENABLE_DRAFTS: drafts (features.drafts)ENABLE_CONTENT_CALENDAR: content calendar (features.contentCalendar)ENABLE_CONTENT_LABELS: labels and campaigns (features.contentLabels)ENABLE_CONTENT_RECYCLING: reuse of archive posts, evergreen collection and the warning about similar posts (features.contentRecycling)ENABLE_CONTENT_INTELLIGENCE: content analysis (features.contentIntelligence)
Labels have one special case: when the feature is off, post endpoints ignore the fields labelIds and campaignId, lists return labels: [] and campaign: null, and the filters labelId and campaignId have no effect.
Scope rule
Almost all endpoints are bound to the user ID from the token. A user therefore sees only their own accounts, posts, drafts, labels, campaigns, dashboards and settings. Foreign IDs lead to 404.
Overview of all documented endpoints
Public
GET /api/healthGET /api/public/configGET /api/public/noticeGET /api/mobile/public/infoGET /api/fedisuite/registry-challenge
Auth
POST /api/auth/registerPOST /api/mobile/auth/registerPOST /api/auth/resend-verificationPOST /api/mobile/auth/resend-verificationPOST /api/auth/verifyPOST /api/auth/loginGET /api/auth/providers/:providerId/startGET /api/auth/providers/:providerId/callbackPOST /api/auth/forgot-passwordPOST /api/auth/reset-passwordPOST /api/auth/fediverse/connectGET /api/auth/fediverse/callbackGET /api/auth/misskey/callbackPOST /api/auth/peertube/connect
Two-factor authentication
POST /api/auth/2fa/email/requestPOST /api/auth/2fa/verifyGET /api/auth/2fa/statusPOST /api/auth/2fa/totp/setupPOST /api/auth/2fa/totp/confirmPOST /api/auth/2fa/totp/disablePOST /api/auth/2fa/recovery/regeneratePOST /api/auth/2fa/email/enablePOST /api/auth/2fa/email/disable
Plugin and provider discovery
GET /api/plugins/discoveryGET /api/plugins/:pluginId/web/manifestGET /api/plugins/:pluginId/web/assets/*GET /api/providers/discoveryPOST /api/providers/:providerId/connectGET /api/providers/:providerId/callbackPOST /api/providers/:providerId/callbackPOST /api/providers/:providerId/disconnectGET /api/plugin-settings/:pluginIdPUT /api/plugin-settings/:pluginId
Accounts and notifications
GET /api/accountsGET /api/accounts/:id/notificationsPOST /api/accounts/:id/notifications/:notificationId/readPOST /api/accounts/:id/notifications/:notificationId/favouritePOST /api/accounts/:id/notifications/:notificationId/replyDELETE /api/accounts/:id
Posts, drafts and calendar
GET /api/postsGET /api/posts/:id/statusGET /api/posts/searchGET /api/posts/calendarPOST /api/postsPOST /api/posts/publish-nowPOST /api/posts/draftsPOST /api/posts/drafts/:idPOST /api/posts/delete-draftsGET /api/posts/:id/edit-sourcePOST /api/posts/:id/editPUT /api/posts/:idPOST /api/posts/:id/publishPOST /api/posts/:id/unschedulePOST /api/posts/:id/copy-to-draftPOST /api/posts/:id/repostDELETE /api/posts/:idPOST /api/posts/from-archivePUT /api/posts/:id/evergreenPOST /api/posts/similar
Labels and campaigns
GET /api/labelsPOST /api/labelsPUT /api/labels/:idDELETE /api/labels/:idGET /api/campaignsPOST /api/campaignsPUT /api/campaigns/:idDELETE /api/campaigns/:id
Account analytics and refresh
POST /api/refresh-statsGET /api/accounts/:id/import-statusGET /api/accounts/:id/top-postsGET /api/accounts/:id/reach-summaryGET /api/accounts/:id/reach-postsGET /api/accounts/:id/posts/:postId/analysisGET /api/accounts/:id/daily-statsGET /api/accounts/:id/stats-historyGET /api/accounts/:id/engagement-rateGET /api/accounts/:id/weekly-growthGET /api/accounts/:id/engagement-breakdownGET /api/accounts/:id/best-timesGET /api/accounts/:id/best-times-quarterhourGET /api/accounts/:id/follower-eventsGET /api/accounts/:id/media-performanceGET /api/accounts/:id/weekday-engagementGET /api/accounts/:id/visibility-breakdownGET /api/accounts/:id/hashtag-overviewGET /api/accounts/:id/top-hashtagsGET /api/accounts/:id/hashtag-combinationsGET /api/accounts/:id/insightsGET /api/accounts/:id/content-insights
User self-service
GET /api/user/dashboard-layoutPUT /api/user/dashboard-layoutGET /api/user/dashboard-periodPUT /api/user/dashboard-periodGET /api/user/dashboard-selected-accountPUT /api/user/dashboard-selected-accountGET /api/user/posts-viewPUT /api/user/posts-viewGET /api/user/profilePUT /api/user/languagePUT /api/user/themePUT /api/user/emailPUT /api/user/passwordPUT /api/user/timezonePUT /api/user/default-accountGET /api/user/sessionsDELETE /api/user/sessions/:sessionIdGET /api/user/data-exportDELETE /api/user/account
Mobile bundles
GET /api/mobile/bootstrapGET /api/mobile/accounts/:id/dashboardPUT /api/mobile/preferences
1. Public API
1.1 GET /api/health
Purpose:
- check availability
- check the database connection
- simplest compatibility check for the Android app
Auth:
- none
Answer on success:
{
"status": "ok",
"db": "connected",
"timestamp": "2026-10-01T12:34:56.000Z"
}
Answer on a database problem (503):
{
"status": "error",
"db": "disconnected",
"error": "connect ECONNREFUSED ..."
}
curl:
curl -sS "https://fedisuite.example.com/api/health"
1.2 GET /api/public/config
Purpose:
- public instance configuration
- state of the feature switches
Auth:
- none
Answer:
{
"enableUserRegistration": true,
"appName": "FediSuite",
"publicSiteUrl": "https://fedisuite.example.com",
"authProviders": [],
"features": {
"drafts": true,
"contentCalendar": true,
"contentLabels": true,
"contentRecycling": true,
"contentIntelligence": true
}
}
curl:
curl -sS "https://fedisuite.example.com/api/public/config"
1.3 GET /api/public/notice
Purpose:
- global public notice of the instance
Auth:
- none
Answer:
{
"enabled": true,
"markdown": "Maintenance today from 22:00."
}
curl:
curl -sS "https://fedisuite.example.com/api/public/notice"
1.4 GET /api/mobile/public/info
Purpose:
- mobile public bundle for login and instance selection
- combines config (including
features), notice and auth capabilities
Auth:
- none
Answer, shortened:
{
"enableUserRegistration": true,
"appName": "FediSuite",
"publicSiteUrl": "https://fedisuite.example.com",
"authProviders": [],
"features": {
"drafts": true,
"contentCalendar": true,
"contentLabels": true,
"contentRecycling": true,
"contentIntelligence": true
},
"notice": {
"enabled": false,
"markdown": ""
},
"connectorProviders": [],
"auth": {
"supportsLogin": true,
"supportsRegistration": true,
"supportsEmailVerification": true,
"supportsResendVerification": true,
"loginIdentifierMode": "email",
"pluginProviders": [],
"pluginAuthProviders": []
}
}
curl:
curl -sS "https://fedisuite.example.com/api/mobile/public/info"
1.5 GET /api/fedisuite/registry-challenge
Purpose:
- proof of ownership for the public FediSuite instance directory
- called by www.fedisuite.com when operators list their instance there
Auth:
- none
Query parameters:
token, the currently active challenge token
Answer:
{ "token": "..." }, but only iftokenmatches the active challenge that has not expired yet- otherwise
404
curl:
curl -sS "https://fedisuite.example.com/api/fedisuite/registry-challenge?token=abc123"
2. Auth API
2.1 POST /api/auth/register
Alias:
POST /api/mobile/auth/register
Purpose:
- create a new user
Request body:
{
"email": "user@example.com",
"password": "secret-password",
"language": "en",
"timezone": "Europe/Berlin"
}
Rules:
emailis normalizedpasswordmust be at least 8 characters longlanguageis optional and normalized as a language tag, without itAccept-Languageappliestimezoneis optional- registration can be switched off on the instance (
403, seeenableUserRegistration) - the admin e-mail address (
ADMIN_EMAIL) is created as verified right away - all other users receive a verification link by e-mail
- an e-mail address that is already taken:
400
Answer:
{
"success": true,
"requiresVerification": true,
"message": "Please confirm your e-mail address first.",
"user": {
"email": "user@example.com",
"language": "en",
"timezone": "Europe/Berlin"
}
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret-password",
"language": "en",
"timezone": "Europe/Berlin"
}'
Mobile alias:
curl -sS \
-X POST "https://fedisuite.example.com/api/mobile/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret-password"
}'
2.2 POST /api/auth/resend-verification
Alias:
POST /api/mobile/auth/resend-verification
Purpose:
- send the verification e-mail again
Request body:
{
"email": "user@example.com"
}
or:
{
"identifier": "user@example.com"
}
Details:
- if the user does not exist, a neutral success answer with
requiresVerification: falsestill comes back - if the account is already verified, success comes back with
alreadyVerified: true
Answers:
{
"success": true,
"requiresVerification": true,
"message": "Please confirm your e-mail address first."
}
or:
{
"success": true,
"requiresVerification": false,
"alreadyVerified": true,
"message": "Your account has been confirmed."
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/resend-verification" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
2.3 POST /api/auth/verify
Purpose:
- complete the e-mail verification
Request body:
{
"token": "hex-token-from-mail"
}
Answer:
{
"success": true,
"message": "Your account has been confirmed."
}
Details:
- the token stays valid after success, so a second call with the same token is harmless and returns success again
- an unknown or missing token:
400
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/verify" \
-H "Content-Type: application/json" \
-d '{
"token": "hex-token-from-mail"
}'
2.4 POST /api/auth/login
Purpose:
- log in with e-mail and password
- returns a JWT or, if two-factor authentication is active, a challenge
Request body:
{
"identifier": "user@example.com",
"password": "my-password"
}
Important:
identifieris the e-mail address, the fieldsemailandusernameare read as alternatives foridentifier- unknown e-mail address:
400 - unverified account or wrong password:
403 - missing fields:
400
Answer without 2FA:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"user": {
"id": 42,
"email": "user@example.com"
},
"isAdmin": false,
"auth": {
"type": "Bearer"
}
}
Answer with active 2FA (no JWT yet):
{
"requires_2fa": true,
"challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"methods": ["totp", "email", "recovery"],
"expires_at": "2026-10-01T12:39:56.000Z"
}
The challenge is valid for five minutes. methods lists the methods the account can use: totp and recovery with an active authenticator, email with an active e-mail code. The flow continues with POST /api/auth/2fa/verify (section 3.2).
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/login" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user@example.com",
"password": "my-password"
}'
2.5 GET /api/auth/providers/:providerId/start
Purpose:
- start of a plugin-based login provider
Auth:
- none
Answer:
- redirect to the login page of the provider if the plugin returns a
redirect_url - otherwise JSON with further start data
- the exact semantics depend on the plugin
Errors:
404if the provider has no start handler500on an error in the plugin
Example:
curl -i "https://fedisuite.example.com/api/auth/providers/demo-login/start"
2.6 GET /api/auth/providers/:providerId/callback
Purpose:
- callback for plugin-based login providers
Auth:
- none
Behavior:
- the plugin returns an identity (
identityoruser), FediSuite creates the user or finds it - a new session is created and a JWT is generated
- a redirect to the app follows, with
auth_tokenandis_adminin the query string - a redirect to a foreign domain is ignored, the target is always the own instance
- if the plugin returns no identity:
400
Important:
- this is a typical browser flow, not a JSON endpoint
Example:
curl -i "https://fedisuite.example.com/api/auth/providers/demo-login/callback?code=abc&state=xyz"
2.7 POST /api/auth/forgot-password
Purpose:
- request a password reset
Request body:
{
"email": "user@example.com"
}
Answer:
{
"message": "If an account exists, an e-mail has been sent."
}
Details:
- the answer deliberately does not distinguish between an existing and a non-existing e-mail address
- the reset token is valid for one hour
- missing
email:400
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/forgot-password" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
2.8 POST /api/auth/reset-password
Purpose:
- set a password with a valid reset token
Request body:
{
"token": "reset-token",
"password": "new-password"
}
Answer:
{
"message": "The password was changed successfully."
}
Rules:
- the new password must be at least 8 characters long
- the token must be valid and must not have expired (otherwise
400)
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/reset-password" \
-H "Content-Type: application/json" \
-d '{
"token": "reset-token",
"password": "new-password"
}'
2.9 POST /api/auth/fediverse/connect
Purpose:
- start the OAuth or MiAuth connection flow for a Fediverse account
Auth:
- bearer token required
Request body:
{
"instanceUrl": "https://mastodon.example"
}
instanceUrl may also be given without https://. The server reduces the URL to its origin and checks it against the policy for outgoing connections (internal and non-public addresses are rejected).
Possible answers:
Regular OAuth instance:
{
"redirectUrl": "https://mastodon.example/oauth/authorize?..."
}
Misskey family:
{
"redirectUrl": "https://misskey.example/miauth/..."
}
PeerTube:
{
"requiresCredentials": true,
"instanceType": "peertube",
"instanceUrl": "https://video.example"
}
The server detects the instance type first. Supported are:
- Mastodon-compatible: Mastodon, Pleroma, Akkoma, GoToSocial, Pixelfed, Friendica, snac, Takahē, BizzFed, vutuv, Mitra and GNU social
- Misskey family: Misskey, Sharkey, Iceshrimp, Calckey and Firefish (MiAuth)
- Vernissage
- Loops
- WordPress (through the ActivityPub plugin)
- PeerTube (credentials instead of a browser redirect, see 2.12)
- platforms that plugins add as providers (see section 4)
Errors:
400if the URL is invalid or not allowed, the instance type is not recognized or the platform is not supported500if the OAuth registration at the instance fails
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/fediverse/connect" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instanceUrl": "https://mastodon.example"
}'
2.10 GET /api/auth/fediverse/callback
Purpose:
- OAuth callback for Mastodon-compatible instances, Loops, Vernissage and WordPress
Auth:
- none, the assignment to the user is carried in
state
Query parameters:
codestate
Behavior:
- exchanges the OAuth code for an access token
- reads the account profile
- creates an account or, when reconnecting (same instance and same username), updates the existing account without losing posts and statistics
- sets
default_account_idif none exists yet - starts the historical import in the background for a new account
- redirects to
/?tab=accountsat the end
Errors (as text, not as JSON):
400for a missing or invalidcodeorstate502if the instance sits behind bot protection or returns no access token500for other errors
Example:
curl -i "https://fedisuite.example.com/api/auth/fediverse/callback?code=abc&state=xyz"
2.11 GET /api/auth/misskey/callback
Purpose:
- MiAuth callback for the Misskey family
Auth:
- none, the assignment to the user is carried in the session ID
Query parameters:
session
Behavior:
- validates the session at the remote instance
- creates or updates the connected account locally
- starts the historical import
- redirects to
/?tab=accounts
Errors (as text): 400 for a missing or invalid session, 500 for other errors.
Example:
curl -i "https://fedisuite.example.com/api/auth/misskey/callback?session=session-id"
2.12 POST /api/auth/peertube/connect
Purpose:
- connect a PeerTube account with username and password
Auth:
- bearer token required
Request body:
{
"instanceUrl": "https://video.example",
"username": "alice",
"password": "secret"
}
Answer:
{
"success": true,
"reconnected": false,
"accountId": 123
}
Details:
- no browser redirect
- the server first fetches the local OAuth client credentials of the PeerTube instance and then signs in with the password
- after that an account is created (or updated, with
reconnected: true) and an import is started for a new account - missing fields or an invalid or disallowed URL:
400
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/peertube/connect" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instanceUrl": "https://video.example",
"username": "alice",
"password": "secret"
}'
3. Two-factor authentication
Three methods are supported: an authenticator (TOTP), an e-mail code and recovery codes. If at least one factor is active, the login (POST /api/auth/login) requires a second step. The management endpoints need the current password and, if TOTP is active, also a current TOTP code. Failed confirmations answer with 403.
3.1 POST /api/auth/2fa/email/request
Purpose:
- request an e-mail code for the second login step
Auth:
- none, the challenge from the login is used instead
Request body:
{
"challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."
}
Answer:
{
"delivered": true,
"expires_at": "2026-10-01T12:44:56.000Z"
}
Rules:
- the code is valid for ten minutes
- a new code can be requested after one minute at the earliest (otherwise
429) - an invalid or expired challenge, or an e-mail code that is not enabled:
400 - delivery failure:
500
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/email/request" \
-H "Content-Type: application/json" \
-d '{
"challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."
}'
3.2 POST /api/auth/2fa/verify
Purpose:
- complete the second login step and receive the JWT
Auth:
- none, the challenge from the login is used instead
Request body:
{
"challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"method": "totp",
"code": "123456"
}
method is totp, email or recovery. With recovery, code holds a recovery code in the format XXXXX-XXXXX, which is used up afterwards.
Answer on success: the same answer as the login without 2FA.
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"user": {
"id": 42,
"email": "user@example.com"
},
"isAdmin": false,
"auth": {
"type": "Bearer"
}
}
Rules:
- the challenge is used up after a success
- wrong code:
403 - an invalid or expired challenge, a missing code, an unsupported method or a factor that is not enabled:
400 - e-mail codes allow five failed attempts, after that the code is used up and a new one has to be requested
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/verify" \
-H "Content-Type: application/json" \
-d '{
"challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"method": "totp",
"code": "123456"
}'
3.3 GET /api/auth/2fa/status
Purpose:
- read the state of two-factor authentication for the own account
Auth:
- bearer token required
Answer:
{
"totp_enabled": true,
"totp_enabled_at": "2026-09-20T08:00:00.000Z",
"email_otp_enabled": false,
"recovery_codes_remaining": 10,
"setup_in_progress": false
}
curl:
curl -sS \
"https://fedisuite.example.com/api/auth/2fa/status" \
-H "Authorization: Bearer $TOKEN"
3.4 POST /api/auth/2fa/totp/setup
Purpose:
- start setting up the authenticator
Auth:
- bearer token required
Request body:
{
"current_password": "my-password"
}
Answer:
{
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_uri": "otpauth://totp/FediSuite:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=FediSuite",
"qr_data_url": "data:image/png;base64,..."
}
Rules:
- the secret is only active after
POST /api/auth/2fa/totp/confirm - if TOTP is already active:
400 - wrong password:
403
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/totp/setup" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "my-password"
}'
3.5 POST /api/auth/2fa/totp/confirm
Purpose:
- complete the setup with a code from the authenticator
Auth:
- bearer token required
Request body:
{
"code": "123456"
}
Answer:
{
"totp_enabled": true,
"recovery_codes": [
"ABCDE-FGHIJ",
"KLMNO-PQRST"
]
}
recovery_codes contains ten codes and is delivered in plain text only this one time. Earlier codes become invalid. Without a previous setup the API answers with 400, with a wrong code with 403.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/totp/confirm" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "123456"
}'
3.6 POST /api/auth/2fa/totp/disable
Purpose:
- disable the authenticator
Auth:
- bearer token required
Request body:
{
"current_password": "my-password",
"current_totp_code": "123456"
}
Answer:
{
"totp_enabled": false
}
The recovery codes are deleted in the process. If TOTP is not active, the API answers with 400.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/totp/disable" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "my-password",
"current_totp_code": "123456"
}'
3.7 POST /api/auth/2fa/recovery/regenerate
Purpose:
- generate new recovery codes, the old ones become invalid
Auth:
- bearer token required
Request body:
{
"current_password": "my-password",
"current_totp_code": "123456"
}
Answer:
{
"recovery_codes": [
"ABCDE-FGHIJ",
"KLMNO-PQRST"
]
}
An active authenticator is required, otherwise 400.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/recovery/regenerate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "my-password",
"current_totp_code": "123456"
}'
3.8 POST /api/auth/2fa/email/enable
Purpose:
- enable the e-mail code as a second factor
Auth:
- bearer token required
Request body:
{
"current_password": "my-password",
"current_totp_code": "123456"
}
current_totp_code is only needed if TOTP is already active.
Answer:
{
"email_otp_enabled": true
}
If the e-mail code is already active, the API answers with 400.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/email/enable" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "my-password"
}'
3.9 POST /api/auth/2fa/email/disable
Purpose:
- disable the e-mail code as a second factor
Auth:
- bearer token required
Request body:
{
"current_password": "my-password",
"current_totp_code": "123456"
}
current_totp_code is only needed if TOTP is active.
Answer:
{
"email_otp_enabled": false
}
Open e-mail codes are discarded in the process. If the e-mail code is not active, the API answers with 400.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/auth/2fa/email/disable" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "my-password"
}'
4. Plugin and provider API
4.1 GET /api/plugins/discovery
Purpose:
- returns the discovery payload of all loaded plugins
Auth:
- bearer token required
Answer:
- discovery payload of the plugin registry
- the structure depends on the loaded plugins
curl:
curl -sS \
"https://fedisuite.example.com/api/plugins/discovery" \
-H "Authorization: Bearer $TOKEN"
4.2 GET /api/plugins/:pluginId/web/manifest
Purpose:
- returns the public web manifest of an active plugin
Auth:
- bearer token required
Errors:
404if the plugin is not active or not started or has no web manifest
curl:
curl -sS \
"https://fedisuite.example.com/api/plugins/fedisuite-plugin-bluesky/web/manifest" \
-H "Authorization: Bearer $TOKEN"
4.3 GET /api/plugins/:pluginId/web/assets/*
Purpose:
- delivers web assets of a plugin
Auth:
- none, so that the browser can load the files as scripts and stylesheets
Behavior:
- the content type is set to match the file
Cache-Control: no-store404as text if the plugin or file is not found,400for an invalid path
curl:
curl -i "https://fedisuite.example.com/api/plugins/fedisuite-plugin-bluesky/web/assets/main.js"
4.4 GET /api/providers/discovery
Purpose:
- returns all connector providers registered by plugins
Auth:
- bearer token required
Answer:
{
"providers": []
}
curl:
curl -sS \
"https://fedisuite.example.com/api/providers/discovery" \
-H "Authorization: Bearer $TOKEN"
4.5 POST /api/providers/:providerId/connect
Purpose:
- starts the connection process for a plugin provider
Auth:
- bearer token required
Answer:
- plugin-specific, often a redirect URL or a start payload
404if the provider has no connect handler
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/providers/demo-provider/connect" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
4.6 GET /api/providers/:providerId/callback and POST /api/providers/:providerId/callback
Purpose:
- callback for plugin providers
Auth:
GET: none, this is the browser return of the providerPOST: bearer token required
Behavior:
- runs the provider callback
- if the plugin returns no
accountand nouser_id, its result is returned unchanged as JSON - otherwise the account is stored, and with
start_import !== falsea historical import starts - after that a redirect (the plugin's
redirect_url) or the JSON answer follows
Typical success answer:
{
"success": true,
"account_id": 123,
"provider_id": "demo-provider"
}
curl:
curl -i "https://fedisuite.example.com/api/providers/demo-provider/callback?code=abc&state=xyz"
or authenticated:
curl -sS \
-X POST "https://fedisuite.example.com/api/providers/demo-provider/callback" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
4.7 POST /api/providers/:providerId/disconnect
Purpose:
- plugin-specific cleanup when disconnecting
Auth:
- bearer token required
Answer:
- plugin-specific
404if the provider has no disconnect handler
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/providers/demo-provider/disconnect" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
4.8 GET /api/plugin-settings/:pluginId
Purpose:
- read plugin-specific settings
Auth:
- bearer token required
Query parameters:
scope=user|account|global, defaultuser- with
scope=accountalsoaccount_id
Important:
scope=globalis only allowed for admins (otherwise403)- permissions are checked against the plugin permissions (otherwise
403) 404if the plugin is not active or the account does not belong to the user400if the plugin knows no settings, the scope is invalid oraccount_idis missing withscope=account
Answer:
{
"plugin_id": "demo-plugin",
"settings_schema": {},
"settings_values": {},
"settings_updated_at": "2026-10-01T12:00:00.000Z",
"scope": "user",
"scope_ref_id": "42"
}
curl:
curl -sS \
"https://fedisuite.example.com/api/plugin-settings/demo-plugin?scope=user" \
-H "Authorization: Bearer $TOKEN"
Account-specific:
curl -sS \
"https://fedisuite.example.com/api/plugin-settings/demo-plugin?scope=account&account_id=123" \
-H "Authorization: Bearer $TOKEN"
4.9 PUT /api/plugin-settings/:pluginId
Purpose:
- write plugin-specific settings
Auth:
- bearer token required
Request body:
{
"scope": "user",
"settings": {
"enabled": true
}
}
With scope=account:
{
"scope": "account",
"account_id": 123,
"settings": {
"enabled": true
}
}
The values are checked against the settingsSchema of the plugin, invalid values lead to 400. As with reading, scope=global is only for admins.
Answer:
{
"plugin_id": "demo-plugin",
"settings_schema": {},
"settings_values": {
"enabled": true
},
"settings_updated_at": "2026-10-01T12:00:00.000Z",
"scope": "user",
"scope_ref_id": "42"
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/plugin-settings/demo-plugin" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "user",
"settings": {
"enabled": true
}
}'
5. Accounts and notifications
5.1 GET /api/accounts
Purpose:
- all connected accounts of the user
Auth:
- bearer token required
Answer, shortened:
[
{
"id": 123,
"instance_url": "https://mastodon.example",
"username": "alice",
"display_name": "Alice Example",
"avatar_url": "https://mastodon.example/media/avatar.jpg",
"stats_followers": 1200,
"stats_following": 300,
"stats_statuses": 800,
"max_characters": 500,
"characters_reserved_per_url": 23,
"max_media_attachments": 4,
"instance_type": "mastodon",
"composer_text_format": "plain",
"import_status": "done",
"auth_error_code": null,
"auth_error_message": null,
"auth_error_at": null,
"created_at": "2026-04-20T10:00:00.000Z",
"is_default": true,
"indexed_posts_count": 780,
"effective_statuses_count": 800,
"scheduled_posts_count": 2,
"failed_posts_count": 0
}
]
stats_followers can be null if the platform hides the value.
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts" \
-H "Authorization: Bearer $TOKEN"
5.2 GET /api/accounts/:id/notifications
Purpose:
- fetch notifications directly from the connected target platform
Auth:
- bearer token required
Query parameters:
limit, default30, maximum80cursor, pagination cursor fromnext_cursor
Supported platforms:
- Mastodon-compatible (without GNU social)
- Misskey, Sharkey and Iceshrimp
- Vernissage
For all other platforms the API answers with 400.
Answer, generic:
{
"items": [],
"next_cursor": null,
"support": {
"supported": true,
"can_reply": true,
"can_favourite": true,
"can_mark_read": true,
"reason": null
}
}
Details:
- with account reauth errors,
401withcode: "account_reauth_required"can come back - foreign or unknown accounts:
404
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/notifications?limit=30" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept-Language: en"
5.3 POST /api/accounts/:id/notifications/:notificationId/read
Purpose:
- mark a notification as read
Auth:
- bearer token required
Supported:
- Mastodon-compatible platforms where
can_mark_readis true - Vernissage
Answer:
{
"success": true
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/accounts/123/notifications/999/read" \
-H "Authorization: Bearer $TOKEN"
5.4 POST /api/accounts/:id/notifications/:notificationId/favourite
Purpose:
- react to the post behind a notification (favourite it)
Auth:
- bearer token required
Request body:
{
"statusId": "114460000000000001"
}
Supported families:
- Mastodon-compatible: favourite
- Misskey family: reaction
- Vernissage: favourite
Answer:
{
"success": true
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/accounts/123/notifications/999/favourite" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"statusId": "114460000000000001"
}'
5.5 POST /api/accounts/:id/notifications/:notificationId/reply
Purpose:
- send a reply to the post of a notification
Auth:
- bearer token required
Request body:
{
"statusId": "114460000000000001",
"content": "Thanks!",
"visibility": "public"
}
Rules:
statusIdis required andcontentmust not be emptyvisibilityis optional, the default ispublic- Misskey does not allow
directorspecifiedhere, the visibilitiespublic,homeandfollowersare possible
Answer:
{
"success": true,
"id": "114460000000000002"
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/accounts/123/notifications/999/reply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"statusId": "114460000000000001",
"content": "Thanks!",
"visibility": "public"
}'
5.6 DELETE /api/accounts/:id
Purpose:
- remove a connected account
Auth:
- bearer token required
Side effects:
- deletes the local posts of this account including media and thumbnails
- statistics, import jobs and follower events of the account are deleted as well
- resets
default_account_idand the selected dashboard account if they pointed to this account - runs plugin hooks and the plugin disconnect cleanup
- posts that are already published on the platform stay there unchanged
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/accounts/123" \
-H "Authorization: Bearer $TOKEN"
6. Posts, drafts and calendar
A post has one of the statuses draft, scheduled, processing, published and failed. During publishing, publish_phase goes through queued, uploading_media, processing_media (for videos that the platform is still processing) and publishing, afterwards it is published or failed. The scheduler does not pick up a post in processing a second time. If it stays unchanged for more than 30 minutes after a crash, it is set back to scheduled.
Posts and drafts with media are sent as multipart/form-data. Images, videos and PDF files can be uploaded (PDF only on platforms that accept it), each file up to 50 MB and at most 20 files per request. The server checks the actual file content, not only the declared content type, and rejects other files with 400. The number of attachments is also limited by max_media_attachments of the account, otherwise 400.
Common form fields, as far as the endpoint reads them:
accountId(for drafts alternativelyaccountIdsas a JSON list, see 6.7)content,spoilerText,title,visibility,languagevisibilityispublic,unlisted,privateordirect, unknown values becomepublicmediaseveral times, plus optionalaltText_0,altText_1, ... andfocusPoint_0,focusPoint_1, ... (formatx,ywith values from -1 to 1)pluginComposerDataas a JSON objectlabelIdsas a JSON list of label IDs (at most 20) andcampaignId, see section 7
6.1 GET /api/posts
Purpose:
- read the queue, drafts and post history of the user
Auth:
- bearer token required
Query parameters:
page, default1pageSize, default20, maximum100search, searches title, text, username and IDstatus=draft|scheduled|published|failed, one tab.scheduledalso includesprocessing. Without it, or with any other value, all posts are returnedsort=asc|desc, defaultdesclabelIdandcampaignId, only posts with this label or this campaignevergreen=true, only drafts of the evergreen collection
Details:
- in the
drafttab, a draft that was saved for several accounts appears only once in the list and carriesgroup_accountswith the accounts of the group - every item contains
labelsandcampaignand the account fieldsaccount_username,account_avatar,account_instance_url,account_instance_typeandaccount_composer_text_format
Answer, shortened:
{
"items": [
{
"id": 991,
"account_id": 123,
"status": "scheduled",
"publish_phase": null,
"scheduled_at": "2026-10-07T08:00:00.000Z",
"title": null,
"content": "Scheduled post",
"spoiler_text": null,
"visibility": "public",
"language": "en",
"thread_group_id": null,
"crosspost_group_id": null,
"draft_stage": null,
"is_evergreen": false,
"account_username": "alice",
"labels": [{ "id": 4, "name": "Release", "color": "#5bc8f5" }],
"campaign": { "id": 2, "name": "Autumn campaign" }
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_previous_page": false,
"has_next_page": false
},
"summary": {
"scheduled": 1,
"processing": 0,
"failed": 0,
"published": 12,
"draft": 3,
"total": 16
},
"filters": {
"search": "",
"status": "",
"sort": "desc",
"label_id": null,
"campaign_id": null
}
}
summary counts all posts of the user, regardless of filters. Drafts that belong to a group count once.
curl:
curl -sS \
"https://fedisuite.example.com/api/posts?page=1&pageSize=20&status=scheduled" \
-H "Authorization: Bearer $TOKEN"
6.2 GET /api/posts/:id/status
Purpose:
- query the status of a post, for example to follow the progress after
publish-now
Auth:
- bearer token required
Answer:
{
"id": 991,
"status": "processing",
"publish_phase": "uploading_media",
"fediverse_id": null,
"error_message": null
}
Errors:
400for an invalid ID404if the post does not exist or belongs to somebody else
curl:
curl -sS \
"https://fedisuite.example.com/api/posts/991/status" \
-H "Authorization: Bearer $TOKEN"
6.3 GET /api/posts/search
Purpose:
- archive search across the posts of the accounts that have been published or imported
Auth:
- bearer token required
Query parameters:
q, search text (several words are searched individually)page, default1, andpageSize, default20, maximum100accountId, only this account (404if it does not belong to the user)includePrivate=true, also searches private and direct posts (they are excluded without it)sort=relevance|date|net_reach|gross_reach|engagement, defaultrelevancewith a search text, otherwisedatesortDir=asc|desc, defaultdesclabelIdandcampaignId
Answer, shortened:
{
"items": [
{
"fediverse_post_id": "114460000000000001",
"account_id": 123,
"content": "<p>Text of the post</p>",
"url": "https://mastodon.example/@alice/114460000000000001",
"created_at": "2025-11-02T09:15:00.000Z",
"is_reply": false,
"total_engagement": 42,
"gross_reach": 5100,
"net_reach": 2300,
"reach_state": "complete",
"hashtags": ["fediverse"],
"account_username": "alice",
"recycling_hint": null
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_previous_page": false,
"has_next_page": false
},
"filters": {
"q": "fediverse",
"accountId": null,
"includePrivate": false,
"sort": "relevance",
"sortDir": "desc"
}
}
recycling_hint is null or an object with age_days and reasons. It marks old posts that reached clearly more than the median of their account (at least 1.5 times). The field is only filled when ENABLE_CONTENT_RECYCLING is on.
curl:
curl -sS \
"https://fedisuite.example.com/api/posts/search?q=fediverse&sort=net_reach" \
-H "Authorization: Bearer $TOKEN"
6.4 GET /api/posts/calendar
Purpose:
- read the posts of a time range for the content calendar
Auth:
- bearer token required
Feature switch:
ENABLE_CONTENT_CALENDAR,404when the feature is off
Query parameters:
from(included) andto(excluded), both ISO-8601 and required. The range may cover at most 45 days, otherwise400accountId, only this accountplatform, only accounts of this platform (for examplemastodon)status, comma-separated list ofdraft,scheduled,processing,publishedandfailed. The default leaves outdraftlabelIdandcampaignId
Answer, shortened:
{
"items": [
{
"id": 991,
"account_id": 123,
"status": "scheduled",
"scheduled_at": "2026-10-07T08:00:00.000Z",
"content": "Scheduled post",
"visibility": "public",
"thread_group_id": null,
"draft_stage": null,
"media_count": 1,
"media_previews": ["/uploads/thumbs/abc.jpg"],
"account_username": "alice",
"account_instance_type": "mastodon",
"labels": [],
"campaign": null
}
],
"truncated": false,
"limit": 500,
"range": {
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-11-01T00:00:00.000Z"
}
}
The entries are sorted by scheduled_at ascending. At most 500 posts are returned, truncated indicates that more match.
curl:
curl -sS \
"https://fedisuite.example.com/api/posts/calendar?from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z&status=scheduled,published" \
-H "Authorization: Bearer $TOKEN"
6.5 POST /api/posts
Purpose:
- schedule a post
- optionally with thread splitting and media
Auth:
- bearer token required
Content type:
multipart/form-data
Form fields:
accountId(required),content,scheduledAt,spoilerText,visibility,title,languageautoSplitThread, splits a long text into a thread (only on platforms with thread support)media,altText_N,focusPoint_N,pluginComposerData,labelIds,campaignId(see the start of section 6)
Rules:
- without
scheduledAtthe current time applies - PeerTube needs a
title languageis validated (400for an invalid language tag)- vutuv does not support certain combinations of visibility and content warning (
400) - the parts of a thread are stored in one transaction
- an unknown account or foreign labels and campaigns:
404
Answer:
- a single post record or
- a thread payload with
thread_group_id,thread_totalanditems
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts" \
-H "Authorization: Bearer $TOKEN" \
-F "accountId=123" \
-F "content=Scheduled post" \
-F "scheduledAt=2026-10-07T08:00:00.000Z" \
-F "visibility=public" \
-F "language=en"
6.6 POST /api/posts/publish-now
Purpose:
- create a post right away and publish it directly
Auth:
- bearer token required
Content type:
multipart/form-data
Form fields:
- as with
POST /api/posts, only withoutscheduledAt
Behavior:
- the answer comes back immediately, the post is
processingwithpublish_phase: "queued" - publishing runs in the background,
GET /api/posts/:id/statusshows the progress
Answer:
- a single post record or
- a thread payload with the segments
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/publish-now" \
-H "Authorization: Bearer $TOKEN" \
-F "accountId=123" \
-F "content=Straight out" \
-F "visibility=public"
6.7 POST /api/posts/drafts
Purpose:
- save a new draft
Auth:
- bearer token required
Feature switch:
ENABLE_DRAFTS
Content type:
multipart/form-data
Form fields:
accountIdsas a JSON list (for example[123,124]) oraccountIdfor one account. At most 20 accounts are possible, a draft is created for each of them and they appear as one entry in the listcontent,title,spoilerText,visibility,languagedraftStagewithidea,in_progressorready, defaultin_progressmedia,altText_N,focusPoint_N,pluginComposerData,labelIds,campaignIdkeptMediaas a JSON list of attachments that are already stored (token,alt,focal), mainly for 6.8
Rules:
- a draft has no time, the scheduler never publishes it
- a draft needs a text or at least one attachment (otherwise
400) - an unknown account or an account of another user:
404
Answer:
{
"primary_id": 1001,
"crosspost_group_id": "7f3c2b1a",
"items": [
{
"id": 1001,
"account_id": 123,
"status": "draft",
"draft_stage": "in_progress",
"content": "Idea for next week",
"scheduled_at": null,
"labels": [],
"campaign": null
}
]
}
primary_id is the draft that the interface edits, crosspost_group_id connects the drafts of one entry.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/drafts" \
-H "Authorization: Bearer $TOKEN" \
-F "accountIds=[123,124]" \
-F "content=Idea for next week" \
-F "draftStage=idea"
6.8 POST /api/posts/drafts/:id
Purpose:
- save changes to a draft
Auth:
- bearer token required
Feature switch:
ENABLE_DRAFTS
Content type:
multipart/form-data
Form fields and rules as in 6.7. The account list replaces the previous selection of the group, so accounts are added or removed. Attachments that should stay are listed in keptMedia, new ones come as media. Labels and campaign stay unchanged if the fields are missing.
Answer:
- as in 6.7
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/drafts/1001" \
-H "Authorization: Bearer $TOKEN" \
-F "accountIds=[123]" \
-F "content=Revised idea" \
-F "draftStage=ready"
6.9 POST /api/posts/delete-drafts
Purpose:
- delete several drafts with one request
Auth:
- bearer token required
Feature switch:
ENABLE_DRAFTS
Request body:
{
"ids": [1001, 1002]
}
Rules:
idsis a list of one to 100 whole numbers greater than 0, otherwise400- only drafts of the own user are deleted, whatever IDs the request names
- a draft that was saved for several accounts is deleted as a whole
- the files of the drafts are deleted as well
Answer:
{
"deleted_ids": [1001, 1002]
}
deleted_ids contains the IDs that were drafts of the user and are gone now.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/delete-drafts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ids": [1001, 1002]
}'
6.10 GET /api/posts/:id/edit-source
Purpose:
- read everything the interface needs to edit a post
Auth:
- bearer token required
Answer, shortened:
{
"id": 991,
"account_id": 123,
"status": "draft",
"draft_stage": "ready",
"crosspost_group_id": "7f3c2b1a",
"group_accounts": [
{ "post_id": 991, "account_id": 123 },
{ "post_id": 992, "account_id": 124 }
],
"is_evergreen": false,
"source": null,
"title": "",
"content": "Text of the draft",
"spoiler_text": "",
"visibility": "public",
"language": "en",
"scheduled_at": null,
"labels": [],
"campaign": null,
"media": [
{
"token": "local:0",
"thumb_url": "/uploads/thumbs/abc.jpg",
"alt": "Alternative text",
"focal": null,
"type": "image"
}
]
}
Details:
group_accountsis only filled for draftssourcedescribes the archive post a draft was made from (id,url,created_at), otherwisenullmedia[].token(local:<index>orremote:<index>) is sent back inkeptMediawhen saving. For published posts whose files are no longer stored, the server reads the attachments from the platform404if the post does not exist or belongs to somebody else
curl:
curl -sS \
"https://fedisuite.example.com/api/posts/991/edit-source" \
-H "Authorization: Bearer $TOKEN"
6.11 POST /api/posts/:id/edit
Purpose:
- edit a post completely (text, settings, attachments, labels) and reschedule it or publish it right away
Auth:
- bearer token required
Content type:
multipart/form-data
Form fields:
action,schedule(default) orpublishscheduledAt, required withscheduleaccountId, optional, if the post should move to another account of the user (a foreign account leads to404)content,title,spoilerText,visibility,languagemedia,altText_N,focusPoint_N,keptMedia,pluginComposerData,labelIds,campaignIdautoSplitThread
Rules:
- posts with the status
scheduled,draft,failedandpublishedcan be edited, otherwise400 - scheduling or publishing a draft turns it into a normal post
- without text and without attachment:
400 - if the edit fails, uploaded files are removed again
Answer:
- the stored post with its new status, for a thread with
thread_group_id,thread_totalanditems - with
action=publishpublishing runs in the background
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/991/edit" \
-H "Authorization: Bearer $TOKEN" \
-F "action=schedule" \
-F "scheduledAt=2026-10-08T10:00:00.000Z" \
-F "content=Revised post" \
-F "visibility=public"
6.12 PUT /api/posts/:id
Purpose:
- change individual fields of a local post (light edit, without attachments)
Auth:
- bearer token required
Request body (JSON, all fields optional):
{
"content": "Updated",
"title": null,
"scheduledAt": "2026-10-08T10:00:00.000Z",
"spoilerText": "CW",
"visibility": "private",
"language": "en"
}
Rules:
- posts with the status
scheduled,draft,failedandpublishedcan be edited - an empty
contentis only allowed if the post has attachments scheduledAtmust be a valid date (400), for posts that are already published the time is not changed- PeerTube posts keep a title
- for a thread,
scheduledAt,spoilerText,visibilityandlanguageapply to all segments that are not published yet - without a single known field:
400 - the change affects the local record, a post that is already published on the platform is not changed by it
Answer:
- the updated post record
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/posts/991" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Updated",
"visibility": "private",
"language": "en"
}'
6.13 POST /api/posts/:id/publish
Purpose:
- publish a stored post right away or retry a failed one
Auth:
- bearer token required
Rules:
- possible with the status
scheduledandfailed, otherwise400 - the post is set to
processingbeforehand so the scheduler cannot publish it a second time - parts of a thread are not supported (
400) 404if the post does not exist or belongs to somebody else
Answer:
- the post record with the status
processing, publishing runs in the background
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/991/publish" \
-H "Authorization: Bearer $TOKEN"
6.14 POST /api/posts/:id/unschedule
Purpose:
- withdraw the plan: a scheduled or failed post becomes a draft without a time again
Auth:
- bearer token required
Feature switch:
ENABLE_DRAFTS
Rules:
- only with the status
scheduledandfailedand only if the post is not part of a thread, otherwise400 - the draft gets the stage
in_progress
Answer:
- the draft as a post record
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/991/unschedule" \
-H "Authorization: Bearer $TOKEN"
6.15 POST /api/posts/:id/copy-to-draft
Purpose:
- copy any post, including a published one, into a new draft
Auth:
- bearer token required
Feature switch:
ENABLE_DRAFTS
Behavior:
- the draft takes over text, content warning, visibility, language, labels and campaign
- attachments are copied, attachments that only exist on the platform are downloaded
- for a draft that was saved for several accounts, a copy is created for each account
- while a post is being published (
processing) copying is not possible (400)
Answer:
- as in 6.7
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/991/copy-to-draft" \
-H "Authorization: Bearer $TOKEN"
6.16 POST /api/posts/:id/repost
Purpose:
- send an existing post again
Auth:
- bearer token required
Behavior:
- copies local media or, if necessary, downloads remote media
- creates a new local record
- publishes immediately
Answer:
{
"success": true,
"postId": 1009,
"fediverseId": "114460000000000099"
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/991/repost" \
-H "Authorization: Bearer $TOKEN"
6.17 DELETE /api/posts/:id
Purpose:
- delete a local post or parts of a thread
Auth:
- bearer token required
Behavior:
- without published thread parts, a complete thread can be deleted
- with published parts, only the matching remaining segments
- a draft that was saved for several accounts is deleted as a group
- media and thumbnails are cleaned up
- a post that does not exist is not an error
- posts that are already published on the platform stay there
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/posts/991" \
-H "Authorization: Bearer $TOKEN"
6.18 POST /api/posts/from-archive
Purpose:
- turn a post from the archive into a new draft (reuse)
Auth:
- bearer token required
Feature switch:
ENABLE_CONTENT_RECYCLINGandENABLE_DRAFTS
Request body:
{
"accountId": 123,
"fediversePostId": "114460000000000001"
}
Behavior:
- the draft takes over the text and, as far as the platform still reports them, content warning, language and attachments
skipped_mediagives the number of attachments that could not be taken over- the draft remembers the archive post as its source (
sourceinedit-source) - missing or invalid details:
400
Answer: as in 6.7, plus skipped_media.
{
"primary_id": 1010,
"crosspost_group_id": null,
"items": [],
"skipped_media": 0
}
items contains the new draft (shortened in the example).
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/from-archive" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountId": 123,
"fediversePostId": "114460000000000001"
}'
6.19 PUT /api/posts/:id/evergreen
Purpose:
- add a draft to the evergreen collection or remove it from there
Auth:
- bearer token required
Feature switch:
ENABLE_CONTENT_RECYCLINGandENABLE_DRAFTS
Request body:
{
"evergreen": true
}
evergreen must be a boolean (otherwise 400). Only drafts can be marked, otherwise 400. The mark applies to all drafts of the group. Nothing is published by it.
Answer:
{
"ids": [1001],
"evergreen": true
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/posts/1001/evergreen" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"evergreen": true
}'
6.20 POST /api/posts/similar
Purpose:
- check whether a text resembles a recently published post
Auth:
- bearer token required
Feature switch:
ENABLE_CONTENT_RECYCLING
Request body:
{
"text": "The text that is about to be published",
"excludePostId": 991
}
Rules:
textmust be a string (otherwise400),excludePostIdis optional and leaves a FediSuite post out of the check, for example the one being edited- the posts of the last 60 days with public or unlisted visibility are compared, without replies
- texts under 40 characters are never compared, from a similarity of 0.8 (scale 0 to 1) a post counts as similar
Answer:
{
"similar": {
"similarity": 0.86,
"published_at": "2026-09-20T09:00:00.000Z",
"url": "https://mastodon.example/@alice/114460000000000001",
"account_id": 123
}
}
similar is null if nothing similar was found. url is null if FediSuite does not know the address.
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/posts/similar" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"text": "The text that is about to be published"
}'
7. Labels and campaigns
A label is an internal tag with a name and an optional color. It is never published and never turned into a hashtag. A campaign has a name, a description, an optional period and labels. A post can carry several labels and one campaign (fields labelIds and campaignId, see section 6).
For all endpoints in this section:
- bearer token required
- feature switch
ENABLE_CONTENT_LABELS,404when the feature is off - names are unique per user, regardless of upper and lower case. A duplicate name leads to
409 - deleting removes the label or campaign from the posts, the posts stay unchanged
- an unknown or foreign ID leads to
404
7.1 GET /api/labels
Purpose:
- all labels of the user, sorted by name
Answer:
{
"items": [
{
"id": 4,
"name": "Release",
"color": "#5bc8f5",
"archived": false,
"post_count": 7
}
]
}
curl:
curl -sS \
"https://fedisuite.example.com/api/labels" \
-H "Authorization: Bearer $TOKEN"
7.2 POST /api/labels
Purpose:
- create a label
Request body:
{
"name": "Release",
"color": "#5bc8f5"
}
Rules:
nameis required, at most 64 characters and without control characterscoloris optional and has the format#rrggbb, an empty value means no color
Answer: the new label (like an entry from 7.1, post_count is 0).
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/labels" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Release",
"color": "#5bc8f5"
}'
7.3 PUT /api/labels/:id
Purpose:
- change a label, only what is in the body is changed
Request body:
{
"name": "Releases",
"color": null,
"archived": true
}
name, color and archived are each optional. Without any of them the API answers with 400.
Answer: the changed label.
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/labels/4" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"archived": true
}'
7.4 DELETE /api/labels/:id
Purpose:
- delete a label
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/labels/4" \
-H "Authorization: Bearer $TOKEN"
7.5 GET /api/campaigns
Purpose:
- all campaigns of the user, sorted by name
Answer:
{
"items": [
{
"id": 2,
"name": "Autumn campaign",
"description": "Posts for the autumn release",
"start_date": "2026-10-01",
"end_date": "2026-10-31",
"archived": false,
"label_ids": [4],
"post_count": 5
}
]
}
curl:
curl -sS \
"https://fedisuite.example.com/api/campaigns" \
-H "Authorization: Bearer $TOKEN"
7.6 POST /api/campaigns
Purpose:
- create a campaign
Request body:
{
"name": "Autumn campaign",
"description": "Posts for the autumn release",
"startDate": "2026-10-01",
"endDate": "2026-10-31",
"labelIds": [4]
}
Rules:
nameis required and has at most 100 charactersdescriptionis optional and has at most 2000 charactersstartDateandendDateare optional in the formatYYYY-MM-DD, the end must not be before the start (400)labelIdsare the labels the campaign brings along. Foreign labels lead to404
Answer: the new campaign (like an entry from 7.5).
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/campaigns" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Autumn campaign",
"startDate": "2026-10-01",
"endDate": "2026-10-31",
"labelIds": [4]
}'
7.7 PUT /api/campaigns/:id
Purpose:
- change a campaign, only what is in the body is changed
Request body: the same fields as in 7.6, plus archived (boolean). labelIds replaces the labels of the campaign completely. Without a single change the API answers with 400.
Answer: the changed campaign.
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/campaigns/2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"endDate": "2026-11-15"
}'
7.8 DELETE /api/campaigns/:id
Purpose:
- delete a campaign, its posts stay without a campaign
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/campaigns/2" \
-H "Authorization: Bearer $TOKEN"
8. Refresh and analytics
All endpoints in this section need a bearer token. If the account in the URL does not belong to the user, the API answers with 404.
The days parameter limits the period to the last days days. If it is missing or 0, the whole period applies, except for reach-summary, reach-posts and content-insights, which have their own defaults (see there).
8.1 POST /api/refresh-stats
Purpose:
- refresh all accounts of the user right away
Behavior:
- calls platform-specific profile and statistics endpoints for each account
- updates followers, following, post counters, profile metadata and platform limits
- writes an entry to
stats_historyon changes (or after six hours) - accounts with a reauth error are skipped
- errors for single accounts do not abort the request, they are only logged
Answer:
{
"success": true
}
curl:
curl -sS \
-X POST "https://fedisuite.example.com/api/refresh-stats" \
-H "Authorization: Bearer $TOKEN"
8.2 GET /api/accounts/:id/import-status
Purpose:
- read the current import status and the latest import job
Answer:
{
"import_status": "done",
"job": {
"status": "completed",
"phase": "posts",
"total_posts_fetched": 500,
"total_pages_fetched": 12,
"total_follow_events_fetched": 0,
"started_at": "2026-10-01T10:00:00.000Z",
"completed_at": "2026-10-01T10:04:00.000Z",
"error_message": null,
"next_retry_at": null,
"retry_count": 0
}
}
job is null if there is no import job yet.
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/import-status" \
-H "Authorization: Bearer $TOKEN"
8.3 GET /api/accounts/:id/top-posts
Purpose:
- the most successful posts of an account
Query parameters:
dayslimit, default10, maximum100sort=favourites_count|reblogs_count|replies_count|total_engagement|net_reach|gross_reach, defaultfavourites_count
Answer:
- array of posts from the archive with
fediverse_post_id,content,url,created_at,favourites_count,reblogs_count,replies_count,visibility,media_count,total_engagement,net_reach,gross_reachandreach_state - empty posts (without text and without media) are not listed
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/top-posts?days=30&limit=10&sort=total_engagement" \
-H "Authorization: Bearer $TOKEN"
8.4 GET /api/accounts/:id/reach-summary
Purpose:
- estimated reach of an account for a period, with a comparison to the period before and a daily series
Query parameters:
days, default30, maximum730,0means the whole period
Answer, shortened:
{
"days": 30,
"current": {
"posts_count": 18,
"analyzed_posts": 18,
"favourites": 320,
"boosts": 74,
"replies": 41,
"partial_posts": 1,
"complete_posts": 17,
"last_reach_fetch_at": "2026-10-01T08:00:00.000Z",
"net_reach": 21500,
"gross_reach": 48200
},
"previous": {
"posts_count": 15
},
"daily": [
{
"date": "2026-09-30",
"interactions": 12,
"analyzed_posts": 3,
"net_reach": 900,
"gross_reach": 2100
}
],
"pending_jobs": 0,
"algorithm_version": "fediwings-heuristic-v2"
}
previous has the same fields as current and is null if days means the whole period. pending_jobs counts the reach calculations of the account that are still open. The reach is an estimate, algorithm_version names the formula used.
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/reach-summary?days=30" \
-H "Authorization: Bearer $TOKEN"
8.5 GET /api/accounts/:id/reach-posts
Purpose:
- posts with their reach details, sorted by one criterion
Query parameters:
days, default30, maximum730,0means the whole periodlimit, default20, maximum100sort=net_reach|gross_reach|boosts|date|favourites_count|reblogs_count|replies_count, defaultnet_reach
Answer:
- array with
fediverse_post_id,content,url,created_at, the counters,total_engagement,gross_reach,net_reach,author_followers,booster_followers,visible_boosters,unattributed_boosts,reach_state(pending,partialorcomplete),reach_errorandreach_fetched_at - only posts with the visibility
publicorunlisted
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/reach-posts?days=90&limit=20&sort=net_reach" \
-H "Authorization: Bearer $TOKEN"
8.6 GET /api/accounts/:id/posts/:postId/analysis
Purpose:
- compare a single post with the rest of the account and show which features go along with more interactions
Query parameters:
daysfor the comparison period, without it the whole period
postId is the fediverse_post_id of the post.
Answer, shortened:
{
"post": {
"id": "114460000000000001",
"text": "Text of the post",
"url": "https://mastodon.example/@alice/114460000000000001",
"created_at": "2026-09-20T09:00:00.000Z",
"favourites": 20,
"boosts": 5,
"replies": 2,
"total_engagement": 27
},
"overall_avg": 14.5,
"comparison_sample": 120,
"vs_average_percent": 86,
"factors": [
{
"key": "media",
"with_avg": 18.2,
"without_avg": 9.1,
"lift_percent": 100,
"sample_with": 60,
"sample_without": 60
}
]
}
factors names the features of the post that change the interactions of the comparison posts, the strongest effect first. Possible key values: media, link, reply, question, hashtags, short_opener, number_opener and long_post. If the post does not exist, the API answers with 404.
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/posts/114460000000000001/analysis?days=180" \
-H "Authorization: Bearer $TOKEN"
8.7 GET /api/accounts/:id/daily-stats
Query parameters:
days
Answer:
[
{
"date": "2026-10-01",
"posts_count": 3,
"total_favourites": 20,
"total_reblogs": 5,
"total_replies": 2
}
]
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/daily-stats?days=30" \
-H "Authorization: Bearer $TOKEN"
8.8 GET /api/accounts/:id/stats-history
Query parameters:
days
Answer:
[
{
"followers": 1200,
"following": 300,
"statuses": 800,
"recorded_at": "2026-10-01T09:00:00.000Z"
}
]
followers is null if the platform hid the number on that day.
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/stats-history?days=30" \
-H "Authorization: Bearer $TOKEN"
8.9 GET /api/accounts/:id/engagement-rate
Answer:
- array with
date,posts_countandengagement_rate(interactions per post and day)
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/engagement-rate?days=30" \
-H "Authorization: Bearer $TOKEN"
8.10 GET /api/accounts/:id/weekly-growth
Answer:
- array with
week,follower_changeandfollowers_end
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/weekly-growth?days=90" \
-H "Authorization: Bearer $TOKEN"
8.11 GET /api/accounts/:id/engagement-breakdown
Answer:
{
"favourites": 320,
"boosts": 74,
"replies": 41
}
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/engagement-breakdown?days=30" \
-H "Authorization: Bearer $TOKEN"
8.12 GET /api/accounts/:id/best-times
Answer:
- array with
day_of_week(0 to 6, Sunday is 0),hour,avg_engagementandpost_count
Notes:
avg_engagementis the median of the interactions here, despite the name- this endpoint calculates the hour in UTC, while the mobile bundle calculates the time windows in the time zone of the user
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/best-times?days=30" \
-H "Authorization: Bearer $TOKEN"
8.13 GET /api/accounts/:id/best-times-quarterhour
Answer:
- array with
slot(0 to 95, one slot is 15 minutes from 00:00 UTC, combined across all weekdays),avg_engagement(median of the interactions) andpost_count
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/best-times-quarterhour?days=90" \
-H "Authorization: Bearer $TOKEN"
8.14 GET /api/accounts/:id/follower-events
Answer:
- array with
date,followers_endandnet_change
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/follower-events?days=30" \
-H "Authorization: Bearer $TOKEN"
8.15 GET /api/accounts/:id/media-performance
Answer:
- array with one entry each for
type: "media"andtype: "text", withavg_engagement,post_count,avg_favourites,avg_boostsandavg_replies
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/media-performance?days=30" \
-H "Authorization: Bearer $TOKEN"
8.16 GET /api/accounts/:id/weekday-engagement
Answer:
- array with
day_of_week(0 to 6, Sunday is 0),avg_engagement,avg_favourites,avg_boosts,avg_repliesandpost_count
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/weekday-engagement?days=30" \
-H "Authorization: Bearer $TOKEN"
8.17 GET /api/accounts/:id/visibility-breakdown
Answer:
- array with
visibilityandpost_count, the largest group first
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/visibility-breakdown?days=30" \
-H "Authorization: Bearer $TOKEN"
8.18 GET /api/accounts/:id/hashtag-overview
Answer:
{
"posts_total": 18,
"posts_with_hashtags": 9,
"posts_without_hashtags": 9,
"hashtag_uses": 22,
"unique_hashtags": 8,
"avg_hashtags_per_post": 1.22,
"avg_hashtags_per_tagged_post": 2.44
}
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/hashtag-overview?days=30" \
-H "Authorization: Bearer $TOKEN"
8.19 GET /api/accounts/:id/top-hashtags
Query parameters:
dayslimit, default12, maximum30sort=total_engagement|avg_engagement|posts_count, defaulttotal_engagement
Answer:
- array with
tag,posts_count,total_favourites,total_boosts,total_replies,total_engagement,avg_engagement,boost_rateandreply_rate
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/top-hashtags?days=30&limit=12&sort=avg_engagement" \
-H "Authorization: Bearer $TOKEN"
8.20 GET /api/accounts/:id/hashtag-combinations
Query parameters:
dayslimit, default10, maximum25
Answer:
- array with
tag_a,tag_b,posts_count,total_engagementandavg_engagement
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/hashtag-combinations?days=30&limit=10" \
-H "Authorization: Bearer $TOKEN"
8.21 GET /api/accounts/:id/insights
Purpose:
- tips about growth and posts, generated on the server
Query parameters:
days
Answer:
- an object with the tips, whose structure can change with the analysis engine. The interface and the mobile bundle display it without relying on individual fields
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/insights?days=30" \
-H "Authorization: Bearer $TOKEN"
8.22 GET /api/accounts/:id/content-insights
Purpose:
- content analysis of an account: compares posts with and without certain features, finds combinations that stand out, evaluates the labels and names time windows of the week with a clearly better reach
Feature switch:
ENABLE_CONTENT_INTELLIGENCE,404when the feature is off
Query parameters:
days, default180, maximum730,0means the whole period
Rules of the evaluation (set centrally, the answer names them under rules):
- a group needs at least 8 posts and at least 20 posts must be compared in total, otherwise
has_enough_postsisfalseanddimensions,patternsandtime_windowsstay empty - a difference counts from 15 percent and has to be statistically sound (Welch t value of at least 2, of at least 3 for combinations) and must remain when the strongest post of the better group is taken away
data_basisislow,medium(from 30 compared posts) orhigh(from 100)- language and content warning are only known for posts that FediSuite sent itself
Answer, shortened:
{
"days": 180,
"timezone": "Europe/Berlin",
"total_posts": 140,
"data_basis": "high",
"has_enough_posts": true,
"rules": {
"min_group_posts": 8,
"min_total_posts": 20,
"min_relative_difference": 0.15,
"min_welch_t": 2,
"min_welch_t_for_patterns": 3,
"medium_data_basis_from": 30,
"high_data_basis_from": 100,
"window_hours": 2,
"good_window_from": 0.4
},
"dimensions": [
{
"dimension": "media",
"known_posts": 140,
"total_posts": 140,
"data_basis": "high",
"groups": [
{
"key": "media",
"posts": 60,
"avg_net_reach": 410.5,
"avg_engagement": 18.2,
"avg_replies": 2.1,
"avg_boosts": 4.3,
"versus_rest": {
"net_reach": { "difference": 0.42, "is_significant": true, "reason": null },
"engagement": { "difference": 0.18, "is_significant": false, "reason": "trivial_effect" }
}
}
]
}
],
"patterns": [
{
"dimensions": [
{ "dimension": "media", "key": "media" },
{ "dimension": "visibility", "key": "public" }
],
"metric": "net_reach",
"difference": 0.55,
"posts": 30,
"other_posts": 110,
"average": 520.1,
"other_average": 335.4,
"data_basis": "high"
}
],
"labels": [
{
"label_id": 4,
"name": "Release",
"color": "#5bc8f5",
"key": "4",
"posts": 12,
"avg_net_reach": 380.0,
"data_basis": "medium",
"trend": { "difference": 0.2, "previous_posts": 9 }
}
],
"time_windows": [
{
"weekday": 2,
"start_hour": 18,
"end_hour": 20,
"level": "good",
"difference": 0.45,
"posts": 14,
"other_posts": 126,
"average": 560.0
}
]
}
Notes on the fields:
dimensions[].dimensionismedia,link,length,visibility,languageorcontent_warning, a dimension without a sufficient data basis is missingversus_rest.<measure>.reasonnames the first rule a difference failed (too_few_posts,no_baseline,trivial_effect,not_significantoroutlier_driven), for differences that passed it isnulldifferenceis relative:0.42means 42 percent more than the restlabels[].trendcompares with the period before and isnullif either of the two periods has too few poststime_windows[].weekdaycounts from 1 (Monday) to 7 (Sunday), the hours apply in the time zone of the user.levelisgoodorabove_average
curl:
curl -sS \
"https://fedisuite.example.com/api/accounts/123/content-insights?days=180" \
-H "Authorization: Bearer $TOKEN"
9. User self-service
9.1 GET /api/user/dashboard-layout
Purpose:
- read the stored dashboard layout
Answer:
- array or
null
curl:
curl -sS \
"https://fedisuite.example.com/api/user/dashboard-layout" \
-H "Authorization: Bearer $TOKEN"
9.2 PUT /api/user/dashboard-layout
Request body:
- an array as the complete layout, otherwise
400
Answer:
{
"success": true
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/dashboard-layout" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '["summary","top_posts","best_times"]'
9.3 GET /api/user/dashboard-period
Purpose:
- read the stored statistics period of the dashboard, across devices
Answer:
- a number out of
0,7,30,90,365,730(0is the whole period) ornull
curl:
curl -sS \
"https://fedisuite.example.com/api/user/dashboard-period" \
-H "Authorization: Bearer $TOKEN"
9.4 PUT /api/user/dashboard-period
Request body:
{
"period": 30
}
Allowed are 0, 7, 30, 90, 365 and 730, otherwise 400.
Answer:
{
"success": true
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/dashboard-period" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"period": 30
}'
9.5 GET /api/user/dashboard-selected-account
Purpose:
- read the account last chosen in the dashboard, across devices and separate from the default account
Answer:
- an account ID or
null
curl:
curl -sS \
"https://fedisuite.example.com/api/user/dashboard-selected-account" \
-H "Authorization: Bearer $TOKEN"
9.6 PUT /api/user/dashboard-selected-account
Request body:
{
"accountId": 123
}
With null or without a value the selection is reset. An account that does not belong to the user leads to 404.
Answer:
{
"success": true,
"dashboard_selected_account_id": 123
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/dashboard-selected-account" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountId": 123
}'
9.7 GET /api/user/posts-view
Purpose:
- read the stored view of the posts page
Answer:
{
"view": "list"
}
view is list (default) or calendar.
curl:
curl -sS \
"https://fedisuite.example.com/api/user/posts-view" \
-H "Authorization: Bearer $TOKEN"
9.8 PUT /api/user/posts-view
Request body:
{
"view": "calendar"
}
Allowed are list and calendar, otherwise 400.
Answer:
{
"view": "calendar"
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/posts-view" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"view": "calendar"
}'
9.9 GET /api/user/profile
Answer:
{
"id": 42,
"email": "user@example.com",
"timezone": "Europe/Berlin",
"default_account_id": 123,
"language": "en",
"theme": "dark"
}
curl:
curl -sS \
"https://fedisuite.example.com/api/user/profile" \
-H "Authorization: Bearer $TOKEN"
9.10 PUT /api/user/language
Request body:
{
"language": "en"
}
language is a language tag such as de, en or pt-BR. It is normalized, a value that does not look like a language tag leads to 400.
Answer:
{
"success": true,
"language": "en"
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/language" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"language": "en"
}'
9.11 PUT /api/user/theme
Request body:
{
"theme": "dark"
}
Allowed are dark and light, otherwise 400.
Answer:
{
"success": true,
"theme": "dark"
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/theme" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"theme": "dark"
}'
9.12 PUT /api/user/email
Request body:
{
"newEmail": "new@example.com",
"currentPassword": "my-password"
}
Answer:
{
"success": true
}
Rules:
- the current password is required (wrong:
403) - the new e-mail address must not already be used by another account (
400)
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/email" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"newEmail": "new@example.com",
"currentPassword": "my-password"
}'
9.13 PUT /api/user/password
Request body:
{
"currentPassword": "old-password",
"newPassword": "new-password"
}
Answer:
{
"success": true
}
Rules:
- both fields are required
- the new password must be at least 8 characters long
- a wrong current password leads to
403
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/password" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"currentPassword": "old-password",
"newPassword": "new-password"
}'
9.14 PUT /api/user/timezone
Request body:
{
"timezone": "Europe/Berlin"
}
timezone must be a valid IANA time zone name, otherwise 400.
Answer:
{
"success": true
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/timezone" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"timezone": "Europe/Berlin"
}'
9.15 PUT /api/user/default-account
Request body:
{
"accountId": 123
}
or to reset it:
{
"accountId": null
}
An account that does not belong to the user leads to 404.
Answer:
{
"success": true,
"default_account_id": 123
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/user/default-account" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountId": 123
}'
9.16 GET /api/user/sessions
Purpose:
- list the active sessions of the user
Answer:
{
"sessions": [
{
"id": "3f2a...64 hex characters...c9",
"ip_address": "203.0.113.7",
"user_agent": "Mozilla/5.0 ...",
"created_at": "2026-09-30T08:00:00.000Z",
"last_seen_at": "2026-10-01T09:30:00.000Z"
}
],
"currentSessionId": "3f2a...64 hex characters...c9"
}
The sessions are sorted by last_seen_at in descending order. last_seen_at is updated at most every five minutes.
curl:
curl -sS \
"https://fedisuite.example.com/api/user/sessions" \
-H "Authorization: Bearer $TOKEN"
9.17 DELETE /api/user/sessions/:sessionId
Purpose:
- revoke a session, the matching JWT is invalid afterwards
Rules:
sessionIdis the ID from 9.16 (64 hexadecimal characters), otherwise400- an unknown or already revoked session:
404
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/user/sessions/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN"
9.18 GET /api/user/data-export
Purpose:
- data export of the user (access request under Art. 15 GDPR and data portability)
Answer:
- a ZIP archive (
application/zip, file namefedisuite-export-YYYY-MM-DD.zip) - it contains
auskunft.md,profil.md,konten.md,beitraege.md,verlauf.md,follower_ereignisse.md,statistiken.mdanddaten.jsonwith the raw data - passwords and OAuth tokens are not included
- the documents in the archive are written in German
curl:
curl -sS \
"https://fedisuite.example.com/api/user/data-export" \
-H "Authorization: Bearer $TOKEN" \
-o fedisuite-export.zip
9.19 DELETE /api/user/account
Purpose:
- delete the user including accounts and posts
Request body:
{
"password": "my-password"
}
Behavior:
- password check (without a password
400, a wrong password403) - media cleanup
- plugin cleanup for all accounts
- deletion of posts, accounts and user in one transaction
Answer:
{
"success": true
}
curl:
curl -sS \
-X DELETE "https://fedisuite.example.com/api/user/account" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"password": "my-password"
}'
10. Mobile bundle API
10.1 GET /api/mobile/bootstrap
Purpose:
- central mobile initial payload
Auth:
- bearer token required
Contains:
server_timepublic_config, withenableUserRegistration,appNameandpublicSiteUrl(the feature switches are inGET /api/public/configandGET /api/mobile/public/info)noticeuser, withis_adminsummary, with counters across all accounts (account_count,total_followers,scheduled_posts,failed_posts,published_posts,draft_posts,total_posts,importing_accounts,accounts_with_auth_errorsand more)accountsmobile_capabilities
Important capabilities:
dashboard_periods: currently[7, 30, 90, 365, 730, 0]top_post_sort_optionstop_hashtag_sort_optionssupports_preferences_batch_update: true
curl:
curl -sS \
"https://fedisuite.example.com/api/mobile/bootstrap" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept-Language: en"
10.2 GET /api/mobile/accounts/:id/dashboard
Purpose:
- complete mobile dashboard bundle for one account
Auth:
- bearer token required
Query parameters:
daystopPostsLimittopPostsSorttopHashtagsLimittopHashtagsSorthashtagCombinationsLimit
Defaults:
days=30,0means the whole periodtopPostsLimit=5(1 to 25)topHashtagsLimit=12(1 to 25)hashtagCombinationsLimit=10(1 to 25)topPostsSort=total_engagementtopHashtagsSort=total_engagement
Allowed sorts:
- top posts:
favourites_count,reblogs_count,replies_count,total_engagement - top hashtags:
total_engagement,avg_engagement,posts_count
Bundle content:
server_timeperiodaccount, with the countersscheduled_posts_count,failed_posts_count,published_posts_count,draft_posts_countand the fields of the latest import (latest_import_*)summarychartstop_postsinsights
Details:
best_timesandweekday_engagementare calculated in the bundle in the time zone of the usereffective_statuses_countis based onGREATEST(stats_statuses, indexed_posts_count)- an unknown account leads to
404, an invalid ID to400
curl:
curl -sS \
"https://fedisuite.example.com/api/mobile/accounts/123/dashboard?days=30&topPostsSort=total_engagement&topHashtagsSort=avg_engagement" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept-Language: en"
10.3 PUT /api/mobile/preferences
Purpose:
- update several user settings in one request
Auth:
- bearer token required
Possible fields:
languagethemetimezonedefaultAccountId
Without any of these fields the API answers with 400. The fields are validated as with the single endpoints.
Answer:
{
"success": true,
"profile": {
"id": 42,
"email": "user@example.com",
"timezone": "Europe/Berlin",
"default_account_id": 123,
"language": "en",
"theme": "dark"
}
}
curl:
curl -sS \
-X PUT "https://fedisuite.example.com/api/mobile/preferences" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"language": "en",
"theme": "dark",
"timezone": "Europe/Berlin",
"defaultAccountId": 123
}'
11. Dynamic plugin routes
Besides the endpoints documented above, FediSuite mounts further routes from plugins under /api/plugins/<pluginId>/<path>.
Important:
- these routes are not wired into the core repository
- they depend on installed and enabled plugins and are resolved from the live registry
- each route decides whether it is reachable without signing in, for signed-in users or only for admins
- their documentation belongs to the respective plugin
Practical consequence:
- this reference is complete for the core API
- for the complete runtime API of a specific instance, the routes of all installed plugins come on top
12. Practical complete workflow
Set the base:
BASE_URL="https://fedisuite.example.com"
Check health:
curl -sS "$BASE_URL/api/health"
Login:
LOGIN_RESPONSE=$(curl -sS \
-X POST "$BASE_URL/api/auth/login" \
-H "Content-Type: application/json" \
-d '{
"identifier": "user@example.com",
"password": "my-password"
}')
Extract the token (for an account without 2FA):
TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.token')
For an account with 2FA the answer contains a challenge_token instead of token. Then the second step follows:
CHALLENGE=$(echo "$LOGIN_RESPONSE" | jq -r '.challenge_token')
TOKEN=$(curl -sS \
-X POST "$BASE_URL/api/auth/2fa/verify" \
-H "Content-Type: application/json" \
-d "{\"challenge_token\": \"$CHALLENGE\", \"method\": \"totp\", \"code\": \"123456\"}" \
| jq -r '.token')
Load the bootstrap:
curl -sS "$BASE_URL/api/mobile/bootstrap" \
-H "Authorization: Bearer $TOKEN"
Load accounts:
curl -sS "$BASE_URL/api/accounts" \
-H "Authorization: Bearer $TOKEN"
Load the dashboard:
curl -sS "$BASE_URL/api/mobile/accounts/123/dashboard?days=30" \
-H "Authorization: Bearer $TOKEN"
Load the queue:
curl -sS "$BASE_URL/api/posts?status=scheduled" \
-H "Authorization: Bearer $TOKEN"
Create a scheduled post:
curl -sS \
-X POST "$BASE_URL/api/posts" \
-H "Authorization: Bearer $TOKEN" \
-F "accountId=123" \
-F "content=Test post" \
-F "visibility=public" \
-F "scheduledAt=2026-10-07T08:00:00.000Z"
Save a draft and schedule it later:
curl -sS \
-X POST "$BASE_URL/api/posts/drafts" \
-H "Authorization: Bearer $TOKEN" \
-F "accountId=123" \
-F "content=Idea for next week"
curl -sS \
-X POST "$BASE_URL/api/posts/1001/edit" \
-H "Authorization: Bearer $TOKEN" \
-F "action=schedule" \
-F "scheduledAt=2026-10-08T10:00:00.000Z" \
-F "content=Idea for next week"
Load the calendar of a month:
curl -sS \
"$BASE_URL/api/posts/calendar?from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z" \
-H "Authorization: Bearer $TOKEN"