Add Flung to your admin portal
A step-by-step build, written for a coding agent: let the people who use your admin portal connect their social accounts and post to them through Flung, with your API key kept on your server.
TL;DR
| Question | Answer |
|---|---|
| What will the portal do? | Show each of your customers a panel to connect, reconnect and disconnect their social accounts, post to those accounts with text, images and video, and show what happened on each platform. |
| What are the parts? | Your server, which holds the API key, keeps one Flung profile per customer and makes connect tokens. The browser, which shows <flung-connect> with a token. And a webhook route, where Flung says what was published. |
| What do I need first? | A full-access Flung API key (flung_sk_...) for the environment you build against. While Flung is in private beta, the Flung team issues it. |
| Which API do I build against? | Preview, https://api-preview.flung.dev, until you go live. Then production, https://api.flung.dev, with a production key. |
| What must never happen? | The API key reaching the browser, or one customer seeing or posting to another customer's accounts. |
1. How the parts fit
- A person signs in to your admin portal, with your own sign-in.
- Your server finds that person's customer, and the Flung profile it made for that customer, by your own customer id.
- The page asks your server for a connect token. Your server checks the session, then asks Flung for a token for that profile and your portal's origin, and hands the token to the page.
- The page shows the connect panel with the token. The panel calls Flung directly, but the token reaches only seven calls (list, read, connect, reconnect and disconnect accounts, read a connect, list platforms), only from your origin, only for that profile, and only until it expires.
- To post, the page sends the post to your server. Your server checks the accounts belong to the customer, and calls Flung with the API key and the profile.
- Flung publishes, and sends signed webhook events to your server as each account succeeds or fails.
| Holds | Browser | Your server | Flung |
|---|---|---|---|
| API key | Never | Yes, in an environment variable | Only its hash |
| Connect token | Yes, for up to an hour | Makes it per request | Checks its signature |
| Platform tokens | Never | Never | Yes, encrypted |
| Profile id | It may see it | Stores it beside your customer | Yes |
2. Get an API key
During the private beta, ask the Flung team for a full-access key for your workspace. They issue it from Flung's staff console and it is shown once. The portal at app.flung.dev signs you in and shows your workspaces, but it does not issue keys yet.
Keys belong to one environment: a preview key works only on https://api-preview.flung.dev, and a production key only on https://api.flung.dev. With one full-access key you can make more, such as one per server, with Create an API key.
Check the key and its workspace:
curl -X GET 'https://api.flung.dev/v1/me' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.me.get()const response = await fetch('https://api.flung.dev/v1/me', { method: 'GET', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()The samples on this page call production. On preview, send the same requests to https://api-preview.flung.dev, or give the SDK baseUrl: 'https://api-preview.flung.dev'.
3. Install and configure
npm install @flung.dev/sdk @flung.dev/connect@flung.dev/sdk runs on your server. @flung.dev/connect runs in the browser and holds no secret.
# A full-access Flung API key (flung_sk_...). Server only: never prefix it with NEXT_PUBLIC_.FLUNG_API_KEY=
# Optional. The API: https://api.flung.dev (the default) or https://api-preview.flung.dev.FLUNG_BASE_URL=
# The origin this app is served from, which connect tokens are made for. http://localhost:3000 when left out.APP_ORIGIN=| Variable | Where | Value |
|---|---|---|
FLUNG_API_KEY | Server only | The flung_sk_ key. Never give it a public prefix such as NEXT_PUBLIC_ or VITE_. |
FLUNG_BASE_URL | Server, and passed to the panel | https://api-preview.flung.dev while building. Leave it empty for production. |
APP_ORIGIN | Server | Your admin portal's origin, such as https://admin.example.com. Connect tokens work only from it. |
FLUNG_WEBHOOK_SECRET | Server only | The whsec_ secret from step 9. |
4. Keep one profile per customer
A profile keeps one customer's accounts apart from the rest of your workspace. Make one the first time a customer opens the page, with your own customer id as its externalId, and store its id beside your customer.
| Request | Body | Answer |
|---|---|---|
POST /v1/profiles | { "name": "Acme Studio", "externalId": "user_8412" }; name is required, externalId is up to 255 characters and unique in the workspace | 201 with { "id": "prof_...", "name", "externalId", "createdAt", "updatedAt" }. 409 external_id_taken when a profile already has that externalId. |
GET /v1/profiles?externalId=user_8412 | None | { "data": [profile], "hasMore": false }. Use it to find the profile again, or after a 409. |
GET /v1/profiles/{id} | None | The profile, or 404. |
PATCH /v1/profiles/{id} | { "name": "..." } or { "externalId": "..." }, or null to clear it | The profile. |
DELETE /v1/profiles/{id} | None | 204. For a customer who leaves: disconnects every account in the profile, cancels its scheduled posts, and drops connects still waiting. It cannot be undone. |
Send each with Authorization: Bearer $FLUNG_API_KEY, and an Idempotency-Key on the POST. The SDK version these docs use has no profile methods yet, so call these with fetch from your server. A workspace may have 1 profile on Free, 10 on Pro and any number on Business; past that, POST /v1/profiles answers 403 plan_limit_reached.
5. Make connect tokens on your server
Add a route the page calls for a token. This one, from a working Next.js app, makes a token for APP_ORIGIN for fifteen minutes:
import { Flung, FlungError } from '@flung.dev/sdk'
/** * `POST /api/connect-token`: a short-lived connect token for this app's own page. The API key stays here on the server; * the token it makes can only list, connect, reconnect and disconnect accounts, from `APP_ORIGIN`, for fifteen minutes. * * In a real app, check who is signed in first, and pass their profile as `profileId` so each person sees only theirs. */export async function POST(): Promise<Response> { const apiKey = process.env.FLUNG_API_KEY if (apiKey === undefined || apiKey === '') return Response.json({ error: 'Set FLUNG_API_KEY in .env.local.' }, { status: 500 }) const flung = new Flung({ apiKey, ...(process.env.FLUNG_BASE_URL ? { baseUrl: process.env.FLUNG_BASE_URL } : {}) }) try { const { token, expiresAt } = await flung.connectTokens.create({ origins: [process.env.APP_ORIGIN || 'http://localhost:3000'], expiresIn: 900 }) return Response.json({ token, expiresAt }, { headers: { 'cache-control': 'no-store' } }) } catch (error) { const status = error instanceof FlungError ? (error.status ?? 502) : 502 return Response.json({ error: error instanceof Error ? error.message : 'Could not make a connect token.' }, { status }) }}In your admin portal, change it in three ways:
- At its top, check your own session. Answer
401when nobody is signed in. - Find the customer's profile from step 4, and add
profileId: profile.idto the call. The token then sees and connects only that profile's accounts, and every account connected with it joins the profile. - Add
platformsto offer only some platforms, if you want.
The whole request, as the API takes it:
curl -X POST 'https://api.flung.dev/v1/connect-tokens' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "origins": [ "https://app.example.com" ], "platforms": [ "x", "linkedin", "bluesky" ]}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.connectTokens.create({ origins: [ 'https://app.example.com', ], platforms: [ 'x', 'linkedin', 'bluesky', ],})const response = await fetch('https://api.flung.dev/v1/connect-tokens', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ origins: [ 'https://app.example.com', ], platforms: [ 'x', 'linkedin', 'bluesky', ], }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()| Field | Meaning |
|---|---|
origins | Required. The pages that will use the token, as origins: https, or http://localhost for trying it out. |
profileId | The customer's profile. Leave it out and the token sees every account in the workspace, which an admin portal for many customers must never do. |
platforms | The platform ids the token may connect. Leave it out for every platform. |
expiresIn | Seconds, from 60 to 86400. One hour by default. |
The answer's token starts flung_ct_. Return it with cache-control: no-store.
6. Show the connect panel
The panel asks your route for a token, and again when one expires:
'use client'
import { useCallback, useState } from 'react'
import { FlungConnect } from '@flung.dev/connect/react'
/** Asks this app's server for a connect token; the panel calls it again when a token expires. */async function connectToken(): Promise<string> { const response = await fetch('/api/connect-token', { method: 'POST' }) const body = (await response.json()) as { token?: string; error?: string } if (!response.ok || body.token === undefined) throw new Error(body.error ?? 'Could not make a connect token.') return body.token}
export function Accounts({ baseUrl }: { baseUrl: string | undefined }) { const [events, setEvents] = useState<string[]>([]) const log = useCallback((line: string) => setEvents((previous) => [line, ...previous].slice(0, 5)), []) return ( <> <FlungConnect token={connectToken} baseUrl={baseUrl} onConnect={({ accounts }) => log(`Connected ${accounts.map((account) => account.username ?? account.id).join(', ')}`)} onDisconnect={({ account }) => log(`Disconnected ${account.username ?? account.id}`)} onError={({ code, message }) => log(`${code}: ${message}`)} /> {events.length > 0 && ( <ul className="events" aria-label="What happened"> {events.map((line, index) => ( <li key={`${index}-${line}`}>{line}</li> ))} </ul> )} </> )}Pass baseUrl from FLUNG_BASE_URL, so the panel calls the same API your server made the token on. A preview token sent to production is refused. The panel lists the profile's accounts, offers the platforms, opens each platform's sign-in in a small window, and asks before it disconnects. onConnect, onDisconnect and onError tell your page what happened; every property, event, style hook and word is in the connect panel reference.
7. Or build your own connect screen
The panel is optional. The same calls work from your own page with the connect token, or from your server with the API key and a profileId in the body. Start a connect, send the person to its authorizationUrl, and give redirectUrl to bring them back to your portal:
curl -X POST 'https://api.flung.dev/v1/connect/linkedin' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "redirectUrl": "https://app.example.com/settings/social", "handle": "alice.bsky.social"}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.accounts.connect({ platform: 'linkedin', redirectUrl: 'https://app.example.com/settings/social', handle: 'alice.bsky.social',})const response = await fetch('https://api.flung.dev/v1/connect/linkedin', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ redirectUrl: 'https://app.example.com/settings/social', handle: 'alice.bsky.social', }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()Then read the session until it is no longer pending:
curl -X GET 'https://api.flung.dev/v1/connect-sessions/cs_aaaaaaaaaaaaaaaaaaaaaaaa' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.connectSessions.get({ id: 'cs_aaaaaaaaaaaaaaaaaaaaaaaa',})const response = await fetch('https://api.flung.dev/v1/connect-sessions/cs_aaaaaaaaaaaaaaaaaaaaaaaa', { method: 'GET', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()On the server the SDK waits for you:
const session = await flung.accounts.connect({ platform: 'linkedin' })console.warn(`Open ${session.authorizationUrl}`)const done = await flung.waitForConnection(session.id)| Session status | Meaning |
|---|---|
pending | The person has not finished on the platform yet. |
connected | Done. accountIds lists the accounts stored. Facebook stores one per Page, and LinkedIn can store the organisations the person administers too. |
failed | error.code says why: access_denied, wrong_account, or the platform's reason. |
expired | The person did not finish by expiresAt. Start again. |
For Mastodon, handle is required: the person's server, such as mastodon.social. For Bluesky it is optional: the handle signing in. POST /v1/connect/{platform} answers 403 plan_limit_reached when the workspace is at its plan's account limit.
8. List accounts, and keep them working
List a customer's accounts on your server with GET /v1/accounts?profileId=prof_.... The profileId filter keeps the list to one customer; the panel's token does this by itself. reconnectBefore finds the accounts that need a person soon:
curl -X GET 'https://api.flung.dev/v1/accounts?reconnectBefore=2026-11-30T00%3A00%3A00Z' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.accounts.list({ reconnectBefore: '2026-11-30T00:00:00Z',})const response = await fetch('https://api.flung.dev/v1/accounts?reconnectBefore=2026-11-30T00%3A00%3A00Z', { method: 'GET', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()Read one account:
curl -X GET 'https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.accounts.get({ id: 'acct_aaaaaaaaaaaaaaaaaaaaaaaa',})const response = await fetch('https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa', { method: 'GET', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()| Field | What to do with it |
|---|---|
status: "active" | Post to it. |
status: "needs_reconnect" | The platform refused the token. Show a Reconnect button; posts to it fail with account_needs_reconnect. |
status: "disconnected" | Someone disconnected it. It stays listed so past posts keep their results. Hide it from your post form. |
renewal | automatic: Flung renews the token itself. manual: only a reconnect renews it. not_needed: it does not expire. |
reconnectBy | The last moment the account works without a reconnect, or null. Flung sends account.reconnect_required_soon a week before. |
profileId | The customer it belongs to. Check it on your server before you post. |
The panel shows Reconnect by itself. To reconnect from your own screen, start one for the account and send the person to its authorizationUrl, as in step 7. It must come back as the same platform user:
curl -X POST 'https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa/reconnect' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "redirectUrl": "https://app.example.com/settings/social"}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.accounts.reconnect({ id: 'acct_aaaaaaaaaaaaaaaaaaaaaaaa', redirectUrl: 'https://app.example.com/settings/social',})const response = await fetch('https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa/reconnect', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ redirectUrl: 'https://app.example.com/settings/social', }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()To disconnect from your own screen, which deletes the stored tokens:
curl -X DELETE 'https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.accounts.disconnect({ id: 'acct_aaaaaaaaaaaaaaaaaaaaaaaa',})const response = await fetch('https://api.flung.dev/v1/accounts/acct_aaaaaaaaaaaaaaaaaaaaaaaa', { method: 'DELETE', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()9. Post with images and video
Posting always runs on your server with the API key. The page sends your server the text, the files and the account ids; your server checks the person may post for that customer, checks every account id is in the customer's profile, and calls Flung.
Upload a file from your server, and wait for it to be ready:
const image = await flung.uploadMedia(await readFile('launch.png'), { contentType: 'image/png', filename: 'launch.png' })await flung.posts.create({ content: 'Launching today.', accountIds, mediaIds: [image.id] })Or have Flung fetch it from a public https address:
curl -X POST 'https://api.flung.dev/v1/media/import' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "url": "https://example.com/launch.jpg"}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.media.import({ url: 'https://example.com/launch.jpg',})const response = await fetch('https://api.flung.dev/v1/media/import', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ url: 'https://example.com/launch.jpg', }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()Then post, with the files' ids in mediaIds and the customer's profile in profileId. Flung refuses the post with validation_failed when an account is not in that profile, so one customer can never post to another's accounts. Leave out publishAt to publish now:
curl -X POST 'https://api.flung.dev/v1/posts' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "content": "Launching today.", "accountIds": [ "acct_aaaaaaaaaaaaaaaaaaaaaaaa" ], "platformOverrides": { "x": { "content": "Launching today. #launch" } }, "variables": { "product": "Flung" }}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.posts.create({ content: 'Launching today.', accountIds: [ 'acct_aaaaaaaaaaaaaaaaaaaaaaaa', ], platformOverrides: { x: { content: 'Launching today. #launch', }, }, variables: { product: 'Flung', },})const response = await fetch('https://api.flung.dev/v1/posts', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ content: 'Launching today.', accountIds: [ 'acct_aaaaaaaaaaaaaaaaaaaaaaaa', ], platformOverrides: { x: { content: 'Launching today. #launch', }, }, variables: { product: 'Flung', }, }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()The answer has a target per account. Each is pending, then publishing, then published with the platform's link, or failed with a reason. Read the post again to see them:
curl -X GET 'https://api.flung.dev/v1/posts/post_aaaaaaaaaaaaaaaaaaaaaaaa' \ --header "Authorization: Bearer $FLUNG_API_KEY"import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.posts.get({ id: 'post_aaaaaaaaaaaaaaaaaaaaaaaa',})const response = await fetch('https://api.flung.dev/v1/posts/post_aaaaaaaaaaaaaaaaaaaaaaaa', { method: 'GET', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, },})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()| To | Use |
|---|---|
| Check a post fits before you send it | List platforms: each platform's limits on text, images and video. A post a platform cannot carry is refused when you create it. |
| Let TikTok creators choose | Read what a post may choose, and show the options before each TikTok post. |
| Pick a Pinterest board | List Pinterest boards. |
| Change or cancel a scheduled post | Change a scheduled post, Cancel a scheduled post. |
| Try the failed accounts again | Retry the failed accounts of a post. |
10. Hear what happened
Register an https endpoint once, from a script or your server, and keep the whsec_ secret it returns in FLUNG_WEBHOOK_SECRET:
curl -X POST 'https://api.flung.dev/v1/webhook-endpoints' \ --header "Authorization: Bearer $FLUNG_API_KEY" \ --header 'Content-Type: application/json' \ --header "Idempotency-Key: $(uuidgen)" \ --data '{ "url": "https://example.com/webhooks/flung", "events": [ "post.published", "post.failed" ]}'import { Flung } from '@flung.dev/sdk'
const flung = new Flung({ apiKey: process.env.FLUNG_API_KEY! })const result = await flung.webhookEndpoints.create({ url: 'https://example.com/webhooks/flung', events: [ 'post.published', 'post.failed', ],})const response = await fetch('https://api.flung.dev/v1/webhook-endpoints', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FLUNG_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ url: 'https://example.com/webhooks/flung', events: [ 'post.published', 'post.failed', ], }),})if (!response.ok) throw new Error((await response.json()).error.message)const result = await response.json()In your webhook route, read the raw body as text and verify it before you trust it:
export async function POST(request: Request): Promise<Response> { const body = await request.text() const event = await verifyWebhook({ body, headers: request.headers, secret: process.env.FLUNG_WEBHOOK_SECRET! }) if (event.type === 'post.failed') console.warn(`Post ${event.data.post.id} failed`) return new Response(null, { status: 204 })}| Event |
|---|
post.published |
post.partially_published |
post.failed |
target.published |
target.failed |
account.connected |
account.disconnected |
account.token_refresh_failed |
account.reconnect_required_soon |
inbox.received |
Every event has an id, a type, a createdAt time and data with the post or the account. Flung retries an event your endpoint did not accept, so store the ids you handled and skip a repeat. Update your own record of the post on target.published, target.failed, post.published, post.partially_published and post.failed, and show a reconnect prompt on account.token_refresh_failed and account.reconnect_required_soon.
11. Errors, limits and plans
Every failure is { "error": { "code", "message", "details" } }. Branch on code; the SDK throws FlungError with the same fields.
| Code | Status | What your portal should do |
|---|---|---|
plan_limit_reached | 403 | Say the workspace is at its plan's limit. details has limit (accounts, postsPerMonth, profiles, mediaBytes or webhookEndpoints), plan, max, used and upgradeUrl, the workspace's Plan page in the Flung portal. |
rate_limited | 429 | Wait for the seconds in Retry-After. The SDK does. |
account_needs_reconnect | 422 | Ask the person to reconnect the account. |
validation_failed | 422 | Show details: it names each field, including accounts outside the profile. |
unknown_accounts, unknown_media | 422 | An id is not in this workspace. |
media_not_ready | 422 | Wait until the file is ready. |
text_too_long, media_required, unsupported_media, media_too_large | 422 | Show the platform's limit from details. |
forbidden | 403 | A connect token used outside its seven calls, its origin or its profile. |
unauthorized | 401 | The key is wrong, revoked, or from the other environment. |
Every list returns a page at a time with hasMore; pass the last id as startingAfter. Every POST takes an Idempotency-Key, so a retry with the same key gets the first answer instead of a second post. The SDK sends one for you.
| Limit | Free | Pro | Business |
|---|---|---|---|
| Connected accounts | 3 | 15 | 100 |
| Posts a month | 30 | 1,000 | 10,000 |
| Profiles | 1 | 10 | No limit |
| Requests a minute, per key | 60 | 300 | 600 |
| Stored files | 1 GB | 25 GB | 250 GB |
| Webhook endpoints | 1 | 10 | 50 |
Pro is 19 US dollars a month and Business 59, bought by a workspace owner on the workspace's Plan page at app.flung.dev.
12. Preview and production
| Service | Address |
|---|---|
| REST API | https://api.flung.dev |
| REST API, preview | https://api-preview.flung.dev |
| MCP server | https://mcp.flung.dev |
| Brand files | https://assets.flung.dev |
| These docs | https://docs.flung.dev |
Build and test on preview with a preview key, then switch FLUNG_API_KEY and FLUNG_BASE_URL together for production. Accounts, profiles, posts and webhooks do not move between them: connect the accounts again on production, and register the webhook endpoint there too.
Checklist before you finish
| Check | How to test it |
|---|---|
| The key stays on the server | Search the built browser code for flung_sk_: there is none. The key is in no public environment variable. |
| The token route is behind your sign-in | Call it signed out: it answers 401. |
| One customer, one profile | Sign in as two customers. Each panel lists only its own accounts, and a post with the other customer's account id is refused. |
| The origin matches | The panel loads accounts on your real portal address. A wrong APP_ORIGIN makes the API refuse the panel's calls. |
| Preview end to end | On https://api-preview.flung.dev: connect an account, see it in the panel, post text, post an image, see post.published arrive and verify. |
| Reconnect | An account with needs_reconnect shows Reconnect, and reconnecting it turns it active. |
| Disconnect | Disconnecting asks first, the account turns disconnected, and your post form hides it. |
| Failures | A post too long for a platform is refused with text_too_long, and your portal shows why. |
| Limits | On Free, a fourth account answers plan_limit_reached, and your portal shows the upgrade link. |
| Retries | A repeated post request with the same Idempotency-Key makes one post. |
| Webhooks | A wrong signature is refused, and a repeated event id is handled once. |
Where next
| To | Read |
|---|---|
| See every attribute and event of the panel | Connect panel |
| Learn the three credentials | Authentication |
| Schedule, change and retry posts | Publish and schedule posts |
| Handle every failure | Errors |
| See every endpoint | API reference |