# Mastodon

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Mastodon by creating a plain FlutterFlow API Group with the instance domain as a variable in the base URL — Mastodon is decentralized so there is no single API host. Use a per-account Bearer access token for single-account apps; route OAuth app registration through a Firebase/Supabase proxy for multi-user apps.

## Mastodon in FlutterFlow: Federation Means the Base URL Is a Variable

Mastodon is unlike any other social platform you have integrated before: there is no single API host. Each Mastodon instance — mastodon.social, fosstodon.org, your company's self-hosted server, or any of thousands of others — runs the same API software and exposes the same REST API at its own domain. The API for mastodon.social lives at `https://mastodon.social/api/v1/...`; the API for fosstodon.org lives at `https://fosstodon.org/api/v1/...`. This is the core FlutterFlow design constraint: if you hardcode one domain in the base URL, your app only ever works with that one server. Make the instance domain a FlutterFlow variable instead.

Beyond the federation model, Mastodon's REST API is genuinely straightforward. Authentication uses OAuth 2.0 with Bearer tokens. Scopes are simple: `read` for reading timelines and accounts, `write` for posting statuses and following, `follow` for managing follows. A personal access token generated in Mastodon Preferences → Development is enough for a single-account app (a personal timeline client or a brand account poster). Multi-user apps where your users each log into their own Mastodon accounts need the OAuth Authorization Code flow, with the client secret handled by a Firebase Cloud Function or Supabase Edge Function.

Mastodon is free and open-source. The API itself carries no cost. Rate limits are set per-instance by the instance administrator — mastodon.social currently allows approximately 300 requests per 5 minutes per authenticated user and 300 per 5 minutes per IP for unauthenticated calls, but these numbers can differ on other instances. Verify the limit for your target instance before building a dashboard with frequent refreshes.

## Before you start

- A Mastodon account on the target instance (e.g. mastodon.social) and access to its Preferences panel
- For single-account apps: an access token generated in Mastodon Preferences → Development → New application
- For multi-user apps: a Firebase project (Blaze plan) or Supabase project for the OAuth proxy
- A FlutterFlow project (any plan supporting API Calls)

## Step-by-step guide

### 1. Generate a per-account access token in Mastodon Preferences

Log into your Mastodon account on the web (e.g. `https://mastodon.social`). Click on your profile picture in the top-left to open your profile, then click 'Preferences' (the gear icon or a link in the sidebar). In Preferences, navigate to 'Development' in the left sidebar. Click 'New application'.

Fill in the application name (e.g. 'My FlutterFlow App') and a website URL — this is informational only, not a redirect URL for this flow. Under 'Scopes', check `read` if you only need to read the timeline, or also check `write` if the app needs to post statuses. The `follow` scope is needed only if the app manages follows/blocks. Click 'Submit'.

On the next screen you will see three values: the Client Key, Client Secret, and Your access token. For a single-account app, copy 'Your access token' — this is all you need. The access token is pre-generated for your own account and is tied to the scopes you selected.

Note: for a single-account app (a personal timeline reader or a brand poster), this token is sufficient and you do not need a Firebase/Supabase proxy. The token has read-only scope if you only selected `read`, so a leaked token cannot post on your behalf. However, if you selected `write`, treat the token with care. For any app where other users log in with their own Mastodon accounts, use the OAuth proxy approach covered in Step 5.

**Expected result:** You have an access token string from Mastodon Preferences. Test it by running `curl -H 'Authorization: Bearer YOUR_TOKEN' https://mastodon.social/api/v1/accounts/verify_credentials` in a terminal — it should return your account JSON.

### 2. Create a FlutterFlow API Group with the instance domain as a variable

In FlutterFlow, open the left navigation and click 'API Calls'. Click '+ Add' and select 'Create API Group'. Name it 'Mastodon'. This is the key configuration step that makes the app work across any Mastodon server: instead of hardcoding a base URL like `https://mastodon.social`, set the base URL to `https://{{instance}}/api/v1`.

Click 'Variables' on the API Group (not on an individual call) and add a group-level variable named `instance` with a default test value of `mastodon.social`. Now every API Call in this group automatically uses the variable — changing `instance` to `fosstodon.org` or any other server requires only one update.

Under 'Headers' at the group level, add a header named `Authorization` with the value `Bearer {{accessToken}}`. Add a second group-level variable named `accessToken` with your test token as the default value. Keeping the token as a group-level header means all API Calls inherit it without repeating it.

This setup lets you store both the instance domain and the access token in App State (set at login) and pass them as group-level variables when you trigger API Calls from Action Flows. A user who logs into fosstodon.org gets the fosstodon.org base URL automatically.

```
{
  "api_group": "Mastodon",
  "base_url": "https://{{instance}}/api/v1",
  "group_variables": [
    { "name": "instance", "type": "String", "default": "mastodon.social" },
    { "name": "accessToken", "type": "String", "default": "YOUR_ACCESS_TOKEN" }
  ],
  "group_headers": [
    { "key": "Authorization", "value": "Bearer {{accessToken}}" }
  ]
}
```

**Expected result:** The Mastodon API Group appears in FlutterFlow with the base URL showing `https://{{instance}}/api/v1` and an Authorization header template. Testing with your token and `mastodon.social` as the instance returns a valid response.

### 3. Add API Calls for home timeline and posting a status

Inside the Mastodon API Group, add your first API Call. Click '+ Add API Call'. Name it 'Get Home Timeline'. Set the method to GET and the endpoint to `/timelines/home`. Add optional query variables: `limit` (default 20) and `max_id` (for pagination — empty by default). Click 'Response & Test', press 'Test API Call', and you will receive an array of toot objects.

Click 'Generate from Response' to create JSON paths automatically. Key paths to map: `$[*].id`, `$[*].content` (HTML — note this contains HTML tags which you will need to strip or render), `$[*].created_at`, `$[*].account.display_name`, `$[*].account.avatar`, `$[*].reblogs_count`, `$[*].favourites_count`. Create a Data Type named 'Toot' with these fields (all Strings except the counts which are Integers).

Add a second API Call named 'Post Status'. Set the method to POST, endpoint `/statuses`. Add a body variable `status` (the toot text) and `visibility` (default `public`). The body is form-encoded: set Content-Type to `application/x-www-form-urlencoded` and the body to `status={{statusText}}&visibility={{visibility}}`.

For reading notifications, add a third API Call named 'Get Notifications', method GET, endpoint `/notifications?limit={{limit}}`. Useful JSON paths: `$[*].type`, `$[*].account.display_name`, `$[*].status.content`.

```
{
  "calls": [
    {
      "name": "Get Home Timeline",
      "method": "GET",
      "endpoint": "/timelines/home?limit={{limit}}&max_id={{maxId}}",
      "variables": [
        { "name": "limit", "type": "Integer", "default": 20 },
        { "name": "maxId", "type": "String", "default": "" }
      ],
      "json_paths": {
        "ids": "$[*].id",
        "contents": "$[*].content",
        "createdAt": "$[*].created_at",
        "displayName": "$[*].account.display_name",
        "avatar": "$[*].account.avatar",
        "reblogsCount": "$[*].reblogs_count",
        "favouritesCount": "$[*].favourites_count"
      }
    },
    {
      "name": "Post Status",
      "method": "POST",
      "endpoint": "/statuses",
      "content_type": "application/x-www-form-urlencoded",
      "body": "status={{statusText}}&visibility={{visibility}}",
      "variables": [
        { "name": "statusText", "type": "String" },
        { "name": "visibility", "type": "String", "default": "public" }
      ]
    },
    {
      "name": "Get Notifications",
      "method": "GET",
      "endpoint": "/notifications?limit={{limit}}",
      "variables": [
        { "name": "limit", "type": "Integer", "default": 20 }
      ]
    }
  ]
}
```

**Expected result:** Testing 'Get Home Timeline' returns an array of toots with content, author, and engagement data. Testing 'Post Status' with a test message creates a new toot on the Mastodon instance.

### 4. Bind the timeline to a feed screen and implement pull-to-refresh

Create a 'Timeline' screen in FlutterFlow. Set the 'Get Home Timeline' API Call as the Backend Query so it fires automatically when the screen loads. In the Backend Query settings, pass the App State `instance` and `accessToken` variables as the group-level variable overrides.

Add a ListView widget to the screen. Inside the list item, add a Row widget. In the Row: a CircleImage widget bound to `avatar` (the author's avatar URL), and a Column containing a Text widget for `displayName` (bold, 14sp), a second Text widget for `content` (replace with an HTML widget if rendering rich text), a third Text widget for `createdAt` formatted as a relative date, and a Row at the bottom with Icons for boost count and favourite count.

Enable pull-to-refresh on the ListView (under ListView settings → Enable Pull to Refresh → Refresh action = Backend Query reload). For pagination, use the `max_id` variable: store the ID of the last toot in the list in Page State, and trigger 'Get Home Timeline' with `max_id` set to that value to load older toots when the user scrolls to the bottom.

For the compose screen: add a TextArea widget bound to a Page State `statusText` variable with a character counter (500 characters is the standard Mastodon limit, though some instances allow more). Add a 'Post' button with an Action Flow: call 'Post Status' → on success, show a Snackbar 'Toot posted!' and navigate back to the timeline.

**Expected result:** The Timeline screen shows the home feed with author avatars, toot content, and engagement counts. Pull-to-refresh reloads the feed. The Compose screen posts a new toot and confirms success.

### 5. Route OAuth app registration through a proxy for multi-user apps

For an app where real users log in with their own Mastodon accounts, you need the OAuth Authorization Code flow. The flow requires a client ID and client secret, which you register per instance by calling `POST /api/v1/apps` on the target instance. The client secret must be kept server-side.

Deploy a Firebase Cloud Function or Supabase Edge Function named `mastodonOauth`. This function handles three steps: (1) Register an OAuth app on the target instance by POSTing to `https://{instance}/api/v1/apps` with your app name, redirect URI, and scopes — returns a client ID and secret which you store in Firestore/Supabase keyed by instance domain. (2) Generate the OAuth authorization URL and return it to FlutterFlow. (3) Exchange the authorization code (after the user approves) for an access token via `POST /oauth/token`.

In FlutterFlow, build an onboarding screen where the user types their instance domain. Trigger a Custom Action (or an API Call to your proxy) to get the OAuth URL for that instance. Open the URL in an in-app WebView or `url_launcher` so the user can approve access. Handle the redirect callback (a deep link into your app) by extracting the authorization code and calling your proxy's token-exchange endpoint. Store the returned access token and instance domain in App State and Firestore/Supabase for the logged-in user.

If building multi-user OAuth feels complex, RapidDev's team builds FlutterFlow integrations like this every week and can set up the whole auth flow — free scoping call at rapidevelopers.com/contact.

```
// Supabase Edge Function — mastodon-oauth (index.ts)
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';

const APP_NAME = 'My FlutterFlow App';
const REDIRECT_URI = 'https://yourapp.com/oauth/callback';
const SCOPES = 'read write';

serve(async (req) => {
  const url = new URL(req.url);
  const instance = url.searchParams.get('instance') || 'mastodon.social';

  // Step 1: Register app on the instance (or look up existing registration)
  if (url.pathname.endsWith('/register')) {
    const r = await fetch(`https://${instance}/api/v1/apps`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_name: APP_NAME,
        redirect_uris: REDIRECT_URI,
        scopes: SCOPES
      })
    });
    const app = await r.json();
    // Store app.client_id and app.client_secret in Supabase table keyed by instance
    return new Response(JSON.stringify({ clientId: app.client_id, instance }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  // Step 2: Exchange authorization code for access token
  if (url.pathname.endsWith('/token') && req.method === 'POST') {
    const body = await req.json();
    // Retrieve client_id and client_secret from Supabase for this instance
    const r = await fetch(`https://${instance}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: body.clientId,
        client_secret: body.clientSecret, // read from Supabase secrets
        code: body.code,
        grant_type: 'authorization_code',
        redirect_uri: REDIRECT_URI
      })
    });
    const token = await r.json();
    return new Response(JSON.stringify({ accessToken: token.access_token }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  return new Response('Not found', { status: 404 });
});
```

**Expected result:** The OAuth proxy can register an app on any Mastodon instance and exchange authorization codes for access tokens. The FlutterFlow onboarding screen can complete the full login flow for a user on any instance.

## Best practices

- Always make the instance domain a FlutterFlow API Group variable, not a hardcoded base URL — Mastodon's federation means there is no universal API host.
- Request only the OAuth scopes your app actually needs — `read` for a timeline viewer, `write` only when the app posts statuses or follows.
- Store the instance domain and access token in App State set at login so they propagate automatically to all Mastodon API Calls via group-level variable overrides.
- Use the `flutter_html` package in a Custom Widget to render Mastodon's HTML content field — do not display raw HTML tags in Text widgets.
- Implement pull-to-refresh on the timeline ListView and use `max_id` pagination to load older toots on scroll, rather than fetching large page limits.
- For multi-user apps, keep the OAuth client secret in a Firebase Cloud Function or Supabase Edge Function — never in the FlutterFlow API Call or app bundle.
- Respect per-instance rate limits (approximately 300 requests per 5 minutes per user — verify with the target instance) by caching timeline data in Page State for at least 15 seconds.
- Test your API Calls against the target instance's domain before building the UI — behavior for edge-case content (long posts, media attachments, content warnings) varies slightly across instances.

## Use cases

### Personal Mastodon timeline reader

A FlutterFlow mobile app for an individual who wants to read their Mastodon home timeline and post new toots. A single per-account access token authenticates the app. The user can scroll their home feed, view individual posts with boosts and favourites counts, and compose a new status from a text input with a post button.

Prompt example:

```
Build a Mastodon timeline app that shows my home feed with each toot's content, author display name, avatar, and the number of boosts and favourites. Include a compose screen to post new toots.
```

### Brand account social posting tool

A FlutterFlow app for a team to draft and publish toots from a company's Mastodon account. The app uses a single shared access token for the brand account, shows a history of recent posts, and includes a compose screen with character count (Mastodon's default limit is 500 characters per toot).

Prompt example:

```
Show the last 20 posts from our company Mastodon account with post date, content, and engagement counts. Include a compose screen with a 500-character counter and a Post button.
```

### Multi-instance Mastodon client app

A FlutterFlow app where users enter their Mastodon instance domain at login and authenticate with OAuth. The app stores the instance domain and access token per user and renders the home timeline and notifications. Because the base URL is a variable, the same screens work across mastodon.social, fosstodon.org, or any other instance the user belongs to.

Prompt example:

```
Let users type their Mastodon instance domain (e.g. mastodon.social) and log in with OAuth. After login, show their home timeline and allow them to post new toots.
```

## Troubleshooting

### HTTP 401 Unauthorized — 'The access token is invalid'

Cause: The Bearer token in the Authorization header is incorrect, has been revoked in Mastodon Preferences, or was generated for a different instance than the one being called.

Solution: Verify that the instance domain in the API Group base URL matches the instance where the token was generated — a mastodon.social token does not work against fosstodon.org. Regenerate the token in Mastodon Preferences → Development if needed. Check that the Authorization header value is formatted exactly as `Bearer YOUR_TOKEN` with a space between Bearer and the token.

### HTTP 403 Forbidden on POST /statuses — 'This action is not allowed'

Cause: The access token was generated with only the `read` scope but the app is attempting to post a status, which requires the `write` scope.

Solution: Go to Mastodon Preferences → Development, open the application, and confirm it has the `write` scope selected. If not, delete the application and create a new one with both `read` and `write` scopes. Copy the new access token and update it in FlutterFlow or App State.

### Toot content displays raw HTML tags like `<p>` and `<a>` in the Text widget

Cause: Mastodon returns the `content` field as HTML, not plain text. FlutterFlow's standard Text widget renders the raw HTML string including tags.

Solution: Use the FlutterFlow HTML widget (available in newer FlutterFlow versions) or add a Custom Widget that wraps the `flutter_html` pub.dev package to render the HTML properly. Alternatively, strip HTML tags in a Custom Function before binding to a Text widget using a simple regex replace.

### XMLHttpRequest error on web preview or CORS error when calling the Mastodon API

Cause: Some Mastodon instances (particularly self-hosted ones) restrict browser-origin CORS requests. FlutterFlow's web preview runs in a browser and is subject to CORS enforcement.

Solution: For web builds, route Mastodon API calls through a Firebase/Supabase proxy that adds appropriate CORS headers. For native iOS/Android builds this is not an issue. Major public instances like mastodon.social generally allow CORS for API calls, but self-hosted instances may not.

## Frequently asked questions

### Which Mastodon instance should I build against?

If you are building a single-account app (a personal client or a brand poster), target the instance where that account lives. If you are building a multi-user app, design the base URL as a variable and let users enter their own instance domain at login. The API is the same across all Mastodon instances — your code works against all of them once the base URL is parameterized.

### Can I stream the Mastodon timeline in real time rather than polling?

Mastodon offers a streaming API via WebSocket and Server-Sent Events (SSE) at `/api/v1/streaming/`. FlutterFlow's API Calls are HTTP request/response and cannot hold open a WebSocket or SSE connection. For real-time updates, write a Dart Custom Action using the `web_socket_channel` pub.dev package. Note that Custom Actions do not run in FlutterFlow's web Test Mode — you must test on a physical device or compiled build. For most apps, polling every 15–30 seconds is simpler and sufficient.

### Does a Mastodon access token expire?

Personal access tokens generated in Mastodon Preferences → Development do not expire by default — they remain valid until revoked in the Preferences panel. OAuth app tokens issued via the Authorization Code flow may have expiration depending on the instance's configuration. Check the token response for an `expires_in` field; if present, implement token refresh in your proxy.

### Can I connect to multiple Mastodon instances in the same FlutterFlow app?

Yes. Because the instance domain is a FlutterFlow variable in the API Group base URL, the same API Calls work for any instance — just change the variable value. Store each user's instance domain and access token in App State (set at login) and pass them as variable overrides when triggering Mastodon API Calls. A single API Group handles all instances without duplication.

### Does this integration work for self-hosted Mastodon instances?

Yes, as long as the self-hosted instance is running standard Mastodon software and is reachable from the internet. Set the instance variable to the self-hosted domain (e.g. `social.yourcompany.com`) and the API Calls work identically. Note that self-hosted instance administrators set their own rate limits, CORS policies, and registration settings — verify these before building against a private instance.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/mastodon
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/mastodon
