Resources Core API As of FediSuite 2.0

FediSuite API

This page documents the HTTP API of the FediSuite application without the admin area. It covers public endpoints, login with a two-factor step and connection flows, accounts, posts, drafts, the calendar, labels and campaigns, analytics, user settings and mobile bundles, each with request examples, JSON responses and curl calls.

Coverage

All endpoints of the core repository except /api/admin/....

Auth

JWT via Authorization: Bearer <JWT>, obtained from POST /api/auth/login, with active 2FA after POST /api/auth/2fa/verify.

Important boundary

Plugins can mount additional routes at runtime. They cannot be derived from the core repository.

Important: This reference is complete for the core API, but not automatically the full runtime API of every possible instance. Installed plugins can provide additional endpoints and need to document those separately.

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.com
  • https://app.fedisuite.com

The examples use https://fedisuite.example.com.

Authentication

Authenticated requests use:

http
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 with 401
  • 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:

  • 401 no token, a revoked session or a token without session data
  • 403 token 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.

http
Accept-Language: de

Time format

Timestamps are usually delivered as ISO-8601 strings, mostly in UTC.

Standard error format

Most often:

json
{
  "error": "Error message"
}

Typical status codes:

  • 400 invalid request
  • 401 missing or invalid token (without a JSON body)
  • 403 access denied, for example a wrong password
  • 404 resource not found or feature switched off on this instance
  • 409 name already taken (labels and campaigns)
  • 429 rate limit
  • 500 internal 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 minutes
  • POST /api/auth/register, POST /api/mobile/auth/register, POST /api/auth/resend-verification and POST /api/mobile/auth/resend-verification: 5 requests per hour, shared between them
  • POST /api/auth/forgot-password: 5 requests in 15 minutes
  • POST /api/auth/reset-password: 10 requests in 15 minutes
  • POST /api/auth/2fa/verify: 15 attempts in 15 minutes
  • POST /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/health
  • GET /api/public/config
  • GET /api/public/notice
  • GET /api/mobile/public/info
  • GET /api/fedisuite/registry-challenge

Auth

  • POST /api/auth/register
  • POST /api/mobile/auth/register
  • POST /api/auth/resend-verification
  • POST /api/mobile/auth/resend-verification
  • POST /api/auth/verify
  • POST /api/auth/login
  • GET /api/auth/providers/:providerId/start
  • GET /api/auth/providers/:providerId/callback
  • POST /api/auth/forgot-password
  • POST /api/auth/reset-password
  • POST /api/auth/fediverse/connect
  • GET /api/auth/fediverse/callback
  • GET /api/auth/misskey/callback
  • POST /api/auth/peertube/connect

Two-factor authentication

  • POST /api/auth/2fa/email/request
  • POST /api/auth/2fa/verify
  • GET /api/auth/2fa/status
  • POST /api/auth/2fa/totp/setup
  • POST /api/auth/2fa/totp/confirm
  • POST /api/auth/2fa/totp/disable
  • POST /api/auth/2fa/recovery/regenerate
  • POST /api/auth/2fa/email/enable
  • POST /api/auth/2fa/email/disable

Plugin and provider discovery

  • GET /api/plugins/discovery
  • GET /api/plugins/:pluginId/web/manifest
  • GET /api/plugins/:pluginId/web/assets/*
  • GET /api/providers/discovery
  • POST /api/providers/:providerId/connect
  • GET /api/providers/:providerId/callback
  • POST /api/providers/:providerId/callback
  • POST /api/providers/:providerId/disconnect
  • GET /api/plugin-settings/:pluginId
  • PUT /api/plugin-settings/:pluginId

Accounts and notifications

  • GET /api/accounts
  • GET /api/accounts/:id/notifications
  • POST /api/accounts/:id/notifications/:notificationId/read
  • POST /api/accounts/:id/notifications/:notificationId/favourite
  • POST /api/accounts/:id/notifications/:notificationId/reply
  • DELETE /api/accounts/:id

Posts, drafts and calendar

  • GET /api/posts
  • GET /api/posts/:id/status
  • GET /api/posts/search
  • GET /api/posts/calendar
  • POST /api/posts
  • POST /api/posts/publish-now
  • POST /api/posts/drafts
  • POST /api/posts/drafts/:id
  • POST /api/posts/delete-drafts
  • GET /api/posts/:id/edit-source
  • POST /api/posts/:id/edit
  • PUT /api/posts/:id
  • POST /api/posts/:id/publish
  • POST /api/posts/:id/unschedule
  • POST /api/posts/:id/copy-to-draft
  • POST /api/posts/:id/repost
  • DELETE /api/posts/:id
  • POST /api/posts/from-archive
  • PUT /api/posts/:id/evergreen
  • POST /api/posts/similar

Labels and campaigns

  • GET /api/labels
  • POST /api/labels
  • PUT /api/labels/:id
  • DELETE /api/labels/:id
  • GET /api/campaigns
  • POST /api/campaigns
  • PUT /api/campaigns/:id
  • DELETE /api/campaigns/:id

Account analytics and refresh

  • POST /api/refresh-stats
  • GET /api/accounts/:id/import-status
  • GET /api/accounts/:id/top-posts
  • GET /api/accounts/:id/reach-summary
  • GET /api/accounts/:id/reach-posts
  • GET /api/accounts/:id/posts/:postId/analysis
  • GET /api/accounts/:id/daily-stats
  • GET /api/accounts/:id/stats-history
  • GET /api/accounts/:id/engagement-rate
  • GET /api/accounts/:id/weekly-growth
  • GET /api/accounts/:id/engagement-breakdown
  • GET /api/accounts/:id/best-times
  • GET /api/accounts/:id/best-times-quarterhour
  • GET /api/accounts/:id/follower-events
  • GET /api/accounts/:id/media-performance
  • GET /api/accounts/:id/weekday-engagement
  • GET /api/accounts/:id/visibility-breakdown
  • GET /api/accounts/:id/hashtag-overview
  • GET /api/accounts/:id/top-hashtags
  • GET /api/accounts/:id/hashtag-combinations
  • GET /api/accounts/:id/insights
  • GET /api/accounts/:id/content-insights

User self-service

  • GET /api/user/dashboard-layout
  • PUT /api/user/dashboard-layout
  • GET /api/user/dashboard-period
  • PUT /api/user/dashboard-period
  • GET /api/user/dashboard-selected-account
  • PUT /api/user/dashboard-selected-account
  • GET /api/user/posts-view
  • PUT /api/user/posts-view
  • GET /api/user/profile
  • PUT /api/user/language
  • PUT /api/user/theme
  • PUT /api/user/email
  • PUT /api/user/password
  • PUT /api/user/timezone
  • PUT /api/user/default-account
  • GET /api/user/sessions
  • DELETE /api/user/sessions/:sessionId
  • GET /api/user/data-export
  • DELETE /api/user/account

Mobile bundles

  • GET /api/mobile/bootstrap
  • GET /api/mobile/accounts/:id/dashboard
  • PUT /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:

json
{
  "status": "ok",
  "db": "connected",
  "timestamp": "2026-10-01T12:34:56.000Z"
}

Answer on a database problem (503):

json
{
  "status": "error",
  "db": "disconnected",
  "error": "connect ECONNREFUSED ..."
}

curl:

bash
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:

json
{
  "enableUserRegistration": true,
  "appName": "FediSuite",
  "publicSiteUrl": "https://fedisuite.example.com",
  "authProviders": [],
  "features": {
    "drafts": true,
    "contentCalendar": true,
    "contentLabels": true,
    "contentRecycling": true,
    "contentIntelligence": true
  }
}

curl:

bash
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:

json
{
  "enabled": true,
  "markdown": "Maintenance today from 22:00."
}

curl:

bash
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:

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

bash
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 if token matches the active challenge that has not expired yet
  • otherwise 404

curl:

bash
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:

json
{
  "email": "user@example.com",
  "password": "secret-password",
  "language": "en",
  "timezone": "Europe/Berlin"
}

Rules:

  • email is normalized
  • password must be at least 8 characters long
  • language is optional and normalized as a language tag, without it Accept-Language applies
  • timezone is optional
  • registration can be switched off on the instance (403, see enableUserRegistration)
  • 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:

json
{
  "success": true,
  "requiresVerification": true,
  "message": "Please confirm your e-mail address first.",
  "user": {
    "email": "user@example.com",
    "language": "en",
    "timezone": "Europe/Berlin"
  }
}

curl:

bash
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:

bash
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:

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

or:

json
{
  "identifier": "user@example.com"
}

Details:

  • if the user does not exist, a neutral success answer with requiresVerification: false still comes back
  • if the account is already verified, success comes back with alreadyVerified: true

Answers:

json
{
  "success": true,
  "requiresVerification": true,
  "message": "Please confirm your e-mail address first."
}

or:

json
{
  "success": true,
  "requiresVerification": false,
  "alreadyVerified": true,
  "message": "Your account has been confirmed."
}

curl:

bash
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:

json
{
  "token": "hex-token-from-mail"
}

Answer:

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

bash
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:

json
{
  "identifier": "user@example.com",
  "password": "my-password"
}

Important:

  • identifier is the e-mail address, the fields email and username are read as alternatives for identifier
  • unknown e-mail address: 400
  • unverified account or wrong password: 403
  • missing fields: 400

Answer without 2FA:

json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
  "user": {
    "id": 42,
    "email": "user@example.com"
  },
  "isAdmin": false,
  "auth": {
    "type": "Bearer"
  }
}

Answer with active 2FA (no JWT yet):

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

bash
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:

  • 404 if the provider has no start handler
  • 500 on an error in the plugin

Example:

bash
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 (identity or user), 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_token and is_admin in 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:

bash
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:

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

Answer:

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

bash
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:

json
{
  "token": "reset-token",
  "password": "new-password"
}

Answer:

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

bash
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:

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

json
{
  "redirectUrl": "https://mastodon.example/oauth/authorize?..."
}

Misskey family:

json
{
  "redirectUrl": "https://misskey.example/miauth/..."
}

PeerTube:

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

  • 400 if the URL is invalid or not allowed, the instance type is not recognized or the platform is not supported
  • 500 if the OAuth registration at the instance fails

curl:

bash
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:

  • code
  • state

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_id if none exists yet
  • starts the historical import in the background for a new account
  • redirects to /?tab=accounts at the end

Errors (as text, not as JSON):

  • 400 for a missing or invalid code or state
  • 502 if the instance sits behind bot protection or returns no access token
  • 500 for other errors

Example:

bash
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:

bash
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:

json
{
  "instanceUrl": "https://video.example",
  "username": "alice",
  "password": "secret"
}

Answer:

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

bash
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:

json
{
  "challenge_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."
}

Answer:

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

bash
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:

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

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

bash
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:

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

bash
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:

json
{
  "current_password": "my-password"
}

Answer:

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

bash
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:

json
{
  "code": "123456"
}

Answer:

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

bash
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:

json
{
  "current_password": "my-password",
  "current_totp_code": "123456"
}

Answer:

json
{
  "totp_enabled": false
}

The recovery codes are deleted in the process. If TOTP is not active, the API answers with 400.

curl:

bash
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:

json
{
  "current_password": "my-password",
  "current_totp_code": "123456"
}

Answer:

json
{
  "recovery_codes": [
    "ABCDE-FGHIJ",
    "KLMNO-PQRST"
  ]
}

An active authenticator is required, otherwise 400.

curl:

bash
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:

json
{
  "current_password": "my-password",
  "current_totp_code": "123456"
}

current_totp_code is only needed if TOTP is already active.

Answer:

json
{
  "email_otp_enabled": true
}

If the e-mail code is already active, the API answers with 400.

curl:

bash
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:

json
{
  "current_password": "my-password",
  "current_totp_code": "123456"
}

current_totp_code is only needed if TOTP is active.

Answer:

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

bash
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:

bash
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:

  • 404 if the plugin is not active or not started or has no web manifest

curl:

bash
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-store
  • 404 as text if the plugin or file is not found, 400 for an invalid path

curl:

bash
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:

json
{
  "providers": []
}

curl:

bash
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
  • 404 if the provider has no connect handler

curl:

bash
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 provider
  • POST: bearer token required

Behavior:

  • runs the provider callback
  • if the plugin returns no account and no user_id, its result is returned unchanged as JSON
  • otherwise the account is stored, and with start_import !== false a historical import starts
  • after that a redirect (the plugin's redirect_url) or the JSON answer follows

Typical success answer:

json
{
  "success": true,
  "account_id": 123,
  "provider_id": "demo-provider"
}

curl:

bash
curl -i "https://fedisuite.example.com/api/providers/demo-provider/callback?code=abc&state=xyz"

or authenticated:

bash
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
  • 404 if the provider has no disconnect handler

curl:

bash
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, default user
  • with scope=account also account_id

Important:

  • scope=global is only allowed for admins (otherwise 403)
  • permissions are checked against the plugin permissions (otherwise 403)
  • 404 if the plugin is not active or the account does not belong to the user
  • 400 if the plugin knows no settings, the scope is invalid or account_id is missing with scope=account

Answer:

json
{
  "plugin_id": "demo-plugin",
  "settings_schema": {},
  "settings_values": {},
  "settings_updated_at": "2026-10-01T12:00:00.000Z",
  "scope": "user",
  "scope_ref_id": "42"
}

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/plugin-settings/demo-plugin?scope=user" \
  -H "Authorization: Bearer $TOKEN"

Account-specific:

bash
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:

json
{
  "scope": "user",
  "settings": {
    "enabled": true
  }
}

With scope=account:

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

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

bash
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:

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

bash
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, default 30, maximum 80
  • cursor, pagination cursor from next_cursor

Supported platforms:

  • Mastodon-compatible (without GNU social)
  • Misskey, Sharkey and Iceshrimp
  • Vernissage

For all other platforms the API answers with 400.

Answer, generic:

json
{
  "items": [],
  "next_cursor": null,
  "support": {
    "supported": true,
    "can_reply": true,
    "can_favourite": true,
    "can_mark_read": true,
    "reason": null
  }
}

Details:

  • with account reauth errors, 401 with code: "account_reauth_required" can come back
  • foreign or unknown accounts: 404

curl:

bash
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_read is true
  • Vernissage

Answer:

json
{
  "success": true
}

curl:

bash
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:

json
{
  "statusId": "114460000000000001"
}

Supported families:

  • Mastodon-compatible: favourite
  • Misskey family: reaction
  • Vernissage: favourite

Answer:

json
{
  "success": true
}

curl:

bash
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:

json
{
  "statusId": "114460000000000001",
  "content": "Thanks!",
  "visibility": "public"
}

Rules:

  • statusId is required and content must not be empty
  • visibility is optional, the default is public
  • Misskey does not allow direct or specified here, the visibilities public, home and followers are possible

Answer:

json
{
  "success": true,
  "id": "114460000000000002"
}

curl:

bash
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_id and 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:

json
{
  "success": true
}

curl:

bash
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 alternatively accountIds as a JSON list, see 6.7)
  • content, spoilerText, title, visibility, language
  • visibility is public, unlisted, private or direct, unknown values become public
  • media several times, plus optional altText_0, altText_1, ... and focusPoint_0, focusPoint_1, ... (format x,y with values from -1 to 1)
  • pluginComposerData as a JSON object
  • labelIds as a JSON list of label IDs (at most 20) and campaignId, 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, default 1
  • pageSize, default 20, maximum 100
  • search, searches title, text, username and ID
  • status=draft|scheduled|published|failed, one tab. scheduled also includes processing. Without it, or with any other value, all posts are returned
  • sort=asc|desc, default desc
  • labelId and campaignId, only posts with this label or this campaign
  • evergreen=true, only drafts of the evergreen collection

Details:

  • in the draft tab, a draft that was saved for several accounts appears only once in the list and carries group_accounts with the accounts of the group
  • every item contains labels and campaign and the account fields account_username, account_avatar, account_instance_url, account_instance_type and account_composer_text_format

Answer, shortened:

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

bash
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:

json
{
  "id": 991,
  "status": "processing",
  "publish_phase": "uploading_media",
  "fediverse_id": null,
  "error_message": null
}

Errors:

  • 400 for an invalid ID
  • 404 if the post does not exist or belongs to somebody else

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/posts/991/status" \
  -H "Authorization: Bearer $TOKEN"

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, default 1, and pageSize, default 20, maximum 100
  • accountId, only this account (404 if 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, default relevance with a search text, otherwise date
  • sortDir=asc|desc, default desc
  • labelId and campaignId

Answer, shortened:

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

bash
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, 404 when the feature is off

Query parameters:

  • from (included) and to (excluded), both ISO-8601 and required. The range may cover at most 45 days, otherwise 400
  • accountId, only this account
  • platform, only accounts of this platform (for example mastodon)
  • status, comma-separated list of draft, scheduled, processing, published and failed. The default leaves out draft
  • labelId and campaignId

Answer, shortened:

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

bash
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, language
  • autoSplitThread, 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 scheduledAt the current time applies
  • PeerTube needs a title
  • language is validated (400 for 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_total and items

curl:

bash
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 without scheduledAt

Behavior:

  • the answer comes back immediately, the post is processing with publish_phase: "queued"
  • publishing runs in the background, GET /api/posts/:id/status shows the progress

Answer:

  • a single post record or
  • a thread payload with the segments

curl:

bash
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:

  • accountIds as a JSON list (for example [123,124]) or accountId for one account. At most 20 accounts are possible, a draft is created for each of them and they appear as one entry in the list
  • content, title, spoilerText, visibility, language
  • draftStage with idea, in_progress or ready, default in_progress
  • media, altText_N, focusPoint_N, pluginComposerData, labelIds, campaignId
  • keptMedia as 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:

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

bash
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:

bash
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:

json
{
  "ids": [1001, 1002]
}

Rules:

  • ids is a list of one to 100 whole numbers greater than 0, otherwise 400
  • 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:

json
{
  "deleted_ids": [1001, 1002]
}

deleted_ids contains the IDs that were drafts of the user and are gone now.

curl:

bash
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:

json
{
  "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_accounts is only filled for drafts
  • source describes the archive post a draft was made from (id, url, created_at), otherwise null
  • media[].token (local:<index> or remote:<index>) is sent back in keptMedia when saving. For published posts whose files are no longer stored, the server reads the attachments from the platform
  • 404 if the post does not exist or belongs to somebody else

curl:

bash
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) or publish
  • scheduledAt, required with schedule
  • accountId, optional, if the post should move to another account of the user (a foreign account leads to 404)
  • content, title, spoilerText, visibility, language
  • media, altText_N, focusPoint_N, keptMedia, pluginComposerData, labelIds, campaignId
  • autoSplitThread

Rules:

  • posts with the status scheduled, draft, failed and published can be edited, otherwise 400
  • 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_total and items
  • with action=publish publishing runs in the background

curl:

bash
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):

json
{
  "content": "Updated",
  "title": null,
  "scheduledAt": "2026-10-08T10:00:00.000Z",
  "spoilerText": "CW",
  "visibility": "private",
  "language": "en"
}

Rules:

  • posts with the status scheduled, draft, failed and published can be edited
  • an empty content is only allowed if the post has attachments
  • scheduledAt must 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, visibility and language apply 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:

bash
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 scheduled and failed, otherwise 400
  • the post is set to processing beforehand so the scheduler cannot publish it a second time
  • parts of a thread are not supported (400)
  • 404 if the post does not exist or belongs to somebody else

Answer:

  • the post record with the status processing, publishing runs in the background

curl:

bash
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 scheduled and failed and only if the post is not part of a thread, otherwise 400
  • the draft gets the stage in_progress

Answer:

  • the draft as a post record

curl:

bash
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:

bash
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:

json
{
  "success": true,
  "postId": 1009,
  "fediverseId": "114460000000000099"
}

curl:

bash
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:

json
{
  "success": true
}

curl:

bash
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_RECYCLING and ENABLE_DRAFTS

Request body:

json
{
  "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_media gives the number of attachments that could not be taken over
  • the draft remembers the archive post as its source (source in edit-source)
  • missing or invalid details: 400

Answer: as in 6.7, plus skipped_media.

json
{
  "primary_id": 1010,
  "crosspost_group_id": null,
  "items": [],
  "skipped_media": 0
}

items contains the new draft (shortened in the example).

curl:

bash
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_RECYCLING and ENABLE_DRAFTS

Request body:

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

json
{
  "ids": [1001],
  "evergreen": true
}

curl:

bash
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:

json
{
  "text": "The text that is about to be published",
  "excludePostId": 991
}

Rules:

  • text must be a string (otherwise 400), excludePostId is 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:

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

bash
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, 404 when 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:

json
{
  "items": [
    {
      "id": 4,
      "name": "Release",
      "color": "#5bc8f5",
      "archived": false,
      "post_count": 7
    }
  ]
}

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/labels" \
  -H "Authorization: Bearer $TOKEN"

7.2 POST /api/labels

Purpose:

  • create a label

Request body:

json
{
  "name": "Release",
  "color": "#5bc8f5"
}

Rules:

  • name is required, at most 64 characters and without control characters
  • color is 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:

bash
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:

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

bash
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:

json
{
  "success": true
}

curl:

bash
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:

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

bash
curl -sS \
  "https://fedisuite.example.com/api/campaigns" \
  -H "Authorization: Bearer $TOKEN"

7.6 POST /api/campaigns

Purpose:

  • create a campaign

Request body:

json
{
  "name": "Autumn campaign",
  "description": "Posts for the autumn release",
  "startDate": "2026-10-01",
  "endDate": "2026-10-31",
  "labelIds": [4]
}

Rules:

  • name is required and has at most 100 characters
  • description is optional and has at most 2000 characters
  • startDate and endDate are optional in the format YYYY-MM-DD, the end must not be before the start (400)
  • labelIds are the labels the campaign brings along. Foreign labels lead to 404

Answer: the new campaign (like an entry from 7.5).

curl:

bash
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:

bash
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:

json
{
  "success": true
}

curl:

bash
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_history on 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:

json
{
  "success": true
}

curl:

bash
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:

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

bash
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:

  • days
  • limit, default 10, maximum 100
  • sort=favourites_count|reblogs_count|replies_count|total_engagement|net_reach|gross_reach, default favourites_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_reach and reach_state
  • empty posts (without text and without media) are not listed

curl:

bash
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, default 30, maximum 730, 0 means the whole period

Answer, shortened:

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

bash
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, default 30, maximum 730, 0 means the whole period
  • limit, default 20, maximum 100
  • sort=net_reach|gross_reach|boosts|date|favourites_count|reblogs_count|replies_count, default net_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, partial or complete), reach_error and reach_fetched_at
  • only posts with the visibility public or unlisted

curl:

bash
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:

  • days for the comparison period, without it the whole period

postId is the fediverse_post_id of the post.

Answer, shortened:

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

bash
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:

json
[
  {
    "date": "2026-10-01",
    "posts_count": 3,
    "total_favourites": 20,
    "total_reblogs": 5,
    "total_replies": 2
  }
]

curl:

bash
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:

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

bash
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_count and engagement_rate (interactions per post and day)

curl:

bash
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_change and followers_end

curl:

bash
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:

json
{
  "favourites": 320,
  "boosts": 74,
  "replies": 41
}

curl:

bash
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_engagement and post_count

Notes:

  • avg_engagement is 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:

bash
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) and post_count

curl:

bash
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_end and net_change

curl:

bash
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" and type: "text", with avg_engagement, post_count, avg_favourites, avg_boosts and avg_replies

curl:

bash
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_replies and post_count

curl:

bash
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 visibility and post_count, the largest group first

curl:

bash
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:

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

bash
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:

  • days
  • limit, default 12, maximum 30
  • sort=total_engagement|avg_engagement|posts_count, default total_engagement

Answer:

  • array with tag, posts_count, total_favourites, total_boosts, total_replies, total_engagement, avg_engagement, boost_rate and reply_rate

curl:

bash
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:

  • days
  • limit, default 10, maximum 25

Answer:

  • array with tag_a, tag_b, posts_count, total_engagement and avg_engagement

curl:

bash
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:

bash
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, 404 when the feature is off

Query parameters:

  • days, default 180, maximum 730, 0 means 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_posts is false and dimensions, patterns and time_windows stay 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_basis is low, medium (from 30 compared posts) or high (from 100)
  • language and content warning are only known for posts that FediSuite sent itself

Answer, shortened:

json
{
  "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[].dimension is media, link, length, visibility, language or content_warning, a dimension without a sufficient data basis is missing
  • versus_rest.<measure>.reason names the first rule a difference failed (too_few_posts, no_baseline, trivial_effect, not_significant or outlier_driven), for differences that passed it is null
  • difference is relative: 0.42 means 42 percent more than the rest
  • labels[].trend compares with the period before and is null if either of the two periods has too few posts
  • time_windows[].weekday counts from 1 (Monday) to 7 (Sunday), the hours apply in the time zone of the user. level is good or above_average

curl:

bash
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:

bash
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:

json
{
  "success": true
}

curl:

bash
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 (0 is the whole period) or null

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/user/dashboard-period" \
  -H "Authorization: Bearer $TOKEN"

9.4 PUT /api/user/dashboard-period

Request body:

json
{
  "period": 30
}

Allowed are 0, 7, 30, 90, 365 and 730, otherwise 400.

Answer:

json
{
  "success": true
}

curl:

bash
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:

bash
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:

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

json
{
  "success": true,
  "dashboard_selected_account_id": 123
}

curl:

bash
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:

json
{
  "view": "list"
}

view is list (default) or calendar.

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/user/posts-view" \
  -H "Authorization: Bearer $TOKEN"

9.8 PUT /api/user/posts-view

Request body:

json
{
  "view": "calendar"
}

Allowed are list and calendar, otherwise 400.

Answer:

json
{
  "view": "calendar"
}

curl:

bash
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:

json
{
  "id": 42,
  "email": "user@example.com",
  "timezone": "Europe/Berlin",
  "default_account_id": 123,
  "language": "en",
  "theme": "dark"
}

curl:

bash
curl -sS \
  "https://fedisuite.example.com/api/user/profile" \
  -H "Authorization: Bearer $TOKEN"

9.10 PUT /api/user/language

Request body:

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

json
{
  "success": true,
  "language": "en"
}

curl:

bash
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:

json
{
  "theme": "dark"
}

Allowed are dark and light, otherwise 400.

Answer:

json
{
  "success": true,
  "theme": "dark"
}

curl:

bash
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:

json
{
  "newEmail": "new@example.com",
  "currentPassword": "my-password"
}

Answer:

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

bash
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:

json
{
  "currentPassword": "old-password",
  "newPassword": "new-password"
}

Answer:

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

bash
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:

json
{
  "timezone": "Europe/Berlin"
}

timezone must be a valid IANA time zone name, otherwise 400.

Answer:

json
{
  "success": true
}

curl:

bash
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:

json
{
  "accountId": 123
}

or to reset it:

json
{
  "accountId": null
}

An account that does not belong to the user leads to 404.

Answer:

json
{
  "success": true,
  "default_account_id": 123
}

curl:

bash
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:

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

bash
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:

  • sessionId is the ID from 9.16 (64 hexadecimal characters), otherwise 400
  • an unknown or already revoked session: 404

Answer:

json
{
  "success": true
}

curl:

bash
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 name fedisuite-export-YYYY-MM-DD.zip)
  • it contains auskunft.md, profil.md, konten.md, beitraege.md, verlauf.md, follower_ereignisse.md, statistiken.md and daten.json with the raw data
  • passwords and OAuth tokens are not included
  • the documents in the archive are written in German

curl:

bash
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:

json
{
  "password": "my-password"
}

Behavior:

  • password check (without a password 400, a wrong password 403)
  • media cleanup
  • plugin cleanup for all accounts
  • deletion of posts, accounts and user in one transaction

Answer:

json
{
  "success": true
}

curl:

bash
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_time
  • public_config, with enableUserRegistration, appName and publicSiteUrl (the feature switches are in GET /api/public/config and GET /api/mobile/public/info)
  • notice
  • user, with is_admin
  • summary, with counters across all accounts (account_count, total_followers, scheduled_posts, failed_posts, published_posts, draft_posts, total_posts, importing_accounts, accounts_with_auth_errors and more)
  • accounts
  • mobile_capabilities

Important capabilities:

  • dashboard_periods: currently [7, 30, 90, 365, 730, 0]
  • top_post_sort_options
  • top_hashtag_sort_options
  • supports_preferences_batch_update: true

curl:

bash
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:

  • days
  • topPostsLimit
  • topPostsSort
  • topHashtagsLimit
  • topHashtagsSort
  • hashtagCombinationsLimit

Defaults:

  • days=30, 0 means the whole period
  • topPostsLimit=5 (1 to 25)
  • topHashtagsLimit=12 (1 to 25)
  • hashtagCombinationsLimit=10 (1 to 25)
  • topPostsSort=total_engagement
  • topHashtagsSort=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_time
  • period
  • account, with the counters scheduled_posts_count, failed_posts_count, published_posts_count, draft_posts_count and the fields of the latest import (latest_import_*)
  • summary
  • charts
  • top_posts
  • insights

Details:

  • best_times and weekday_engagement are calculated in the bundle in the time zone of the user
  • effective_statuses_count is based on GREATEST(stats_statuses, indexed_posts_count)
  • an unknown account leads to 404, an invalid ID to 400

curl:

bash
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:

  • language
  • theme
  • timezone
  • defaultAccountId

Without any of these fields the API answers with 400. The fields are validated as with the single endpoints.

Answer:

json
{
  "success": true,
  "profile": {
    "id": 42,
    "email": "user@example.com",
    "timezone": "Europe/Berlin",
    "default_account_id": 123,
    "language": "en",
    "theme": "dark"
  }
}

curl:

bash
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:

bash
BASE_URL="https://fedisuite.example.com"

Check health:

bash
curl -sS "$BASE_URL/api/health"

Login:

bash
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):

bash
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:

bash
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:

bash
curl -sS "$BASE_URL/api/mobile/bootstrap" \
  -H "Authorization: Bearer $TOKEN"

Load accounts:

bash
curl -sS "$BASE_URL/api/accounts" \
  -H "Authorization: Bearer $TOKEN"

Load the dashboard:

bash
curl -sS "$BASE_URL/api/mobile/accounts/123/dashboard?days=30" \
  -H "Authorization: Bearer $TOKEN"

Load the queue:

bash
curl -sS "$BASE_URL/api/posts?status=scheduled" \
  -H "Authorization: Bearer $TOKEN"

Create a scheduled post:

bash
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:

bash
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:

bash
curl -sS \
  "$BASE_URL/api/posts/calendar?from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z" \
  -H "Authorization: Bearer $TOKEN"