Accounts
List connected social accounts, check their health, and refresh credentials.
An account is one connected profile on one network, for example a LinkedIn company page or one of several X accounts in the same workspace. Posts target account ids, so this is usually the first call an integration makes. Everything here needs the accounts scope.
List accounts
GET /v1/accountscurl https://api.fopost.com/v1/accounts \
-H "X-API-Key: $FOPOST_API_KEY"Each account carries its id, platform, username, display name, avatar, and whether it is active. name is the name you gave the account when it has been renamed, and platform_name is always the network's own. Pass group_id to list only the accounts in one account group. A workspace-bound key sees only that workspace's accounts.
Rename an account
PATCH /v1/accounts/{id}curl -X PATCH https://api.fopost.com/v1/accounts/{id} \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "display_name": "Brand UK" }'The new name shows everywhere the account is listed. It is yours, so a profile refresh from the network never overwrites it. Send null or an empty string to go back to the network's name.
Move to another workspace
POST /v1/accounts/{id}/movecurl -X POST https://api.fopost.com/v1/accounts/{id}/move \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "workspace_id": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90" }'The account moves with its connection, so nothing has to be reauthorized, and its inbox and analytics history move with it in the same step. You must own both workspaces; any other target answers 404.
A move is all or nothing. When the account still has records that belong to its current workspace, such as posts, recycling queues, ads, or monitoring rules, the move is refused with 409 move_blocked and a blocking_tables list naming them. A target that already has an account on the same network also answers 409. The account leaves its account groups, since those belong to the old workspace.
Account groups
A group is a named set of accounts in one workspace, such as "EU brands" or "Founders". Target a whole group when posting instead of listing every account.
GET /v1/account-groups
POST /v1/account-groups
GET /v1/account-groups/{id}
PATCH /v1/account-groups/{id}
DELETE /v1/account-groups/{id}
PUT /v1/account-groups/{id}/memberscurl -X POST https://api.fopost.com/v1/account-groups \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
"name": "EU brands",
"account_ids": ["3a9f1d20-6c77-4b2e-9a01-8f5d2c3b4e10"]
}'Each group carries its id, name and account_ids. PUT …/members replaces the member list with the account_ids you send. PATCH renames the group, and DELETE removes the group while its accounts stay connected. Every account must belong to the group's workspace.
To post to a group, pass account_group_id on create post. Each account publishes once, even when it is also listed in accounts.
Connecting an account
Connecting is an OAuth handshake with the network, so it happens in a browser. Your own accounts connect in the dashboard under Accounts → Connect. For your users' accounts, mint a hosted connect link and hand it to them.
POST /v1/accounts exists for the handful of networks that authenticate with credentials you already hold rather than an OAuth redirect.
Hosted connect links
A connect link is a single-use URL that lets one of your end users connect a social account into your workspace, with no FoPost login and under your branding. Use it when your product needs its users' accounts, not yours.
POST /v1/connect-sessionscurl https://api.fopost.com/v1/connect-sessions \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspaceId": "ws_…",
"externalUserId": "user_8812",
"platforms": ["twitter", "linkedin"],
"redirectUrl": "https://app.example.com/settings/social",
"branding": { "name": "Acme", "logoUrl": "https://app.example.com/logo.png" }
}'The response carries a url. Send your user there: they see your name and logo, pick a network from platforms, and authorize it. The link stays open for one hour and is spent by the first account it connects.
externalUserIdis your id for that user. The connected account carries it, soGET /v1/accounts?external_user_id=user_8812finds their accounts.redirectUrlis where they land once connected, withstatus=success,platform,account_idandexternal_user_idappended. Its domain must be on the workspace redirect allowlist, set in the dashboard under API Keys; a domain that is not listed is refused, both when the link is created and again when it finishes.workspaceIdcan be omitted for a key bound to one workspace.platformsaccepts the networks that connect in one step:twitter,instagram,linkedin,threads,tiktok,youtube,reddit,tumblranddribbble. Networks that need a Page or board picked after authorizing are not available through a hosted link yet.
To learn the outcome without a redirect, poll the session:
GET /v1/connect-sessions/{id}status moves from pending to used, and account_id is set, once the user has connected. A session created in another workspace answers 404.
Health
Tokens expire, users revoke apps, and networks change what a permission covers. Rather than discovering that at publish time, read health:
GET /v1/accounts/health
GET /v1/accounts/{id}/healthThe summary endpoint takes an optional workspace_id and reports every account at once, which is what you want on a dashboard or a morning check. A degraded account still publishes, with a warning. An expired or revoked one fails immediately.
POST /v1/accounts/{id}/validate
POST /v1/accounts/{id}/refresh-tokenvalidate calls the network and tells you whether the stored credentials still work. refresh-token forces a refresh ahead of schedule. Neither is normally necessary: tokens refresh on their own in the background, and this is for when you want to check before a launch rather than after.
Analytics for one account
GET /v1/accounts/{id}/analyticsFollower and engagement history for a single account. For cross-account reporting, use the analytics endpoints.
Other operations
| Endpoint | What it does |
|---|---|
GET /v1/accounts/{id} | One account |
POST /v1/accounts/{id}/primary | Mark the account primary for its platform |
DELETE /v1/accounts/{id} | Disconnect. Published posts stay published |
X communities
X accounts can post into communities, which have to be resolved before you can target one:
GET /v1/accounts/{accountId}/communities
GET /v1/accounts/{accountId}/communities/search
POST /v1/accounts/{accountId}/communities/sync
POST /v1/accounts/{accountId}/communities/manual
DELETE /v1/accounts/{accountId}/communities/{communityId}sync pulls the communities the account belongs to, search looks one up, and manual adds one by id when you already know it.
X follow
POST /v1/accounts/{id}/x/follow
POST /v1/accounts/{id}/x/unfollowFollows or unfollows an X user from the account, by the user's numeric X id in user_id. Both need the inbox and publish scopes, and each is one metered X call. An account connected before FoPost asked for the follow permission answers 409 with reconnect_required until it is reconnected.
curl -X POST https://api.fopost.com/v1/accounts/0f1c4a1e-8f3c-4c62-9f6a-2a4b1a9e77d1/x/follow \
-H "X-API-Key: $FOPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "user_id": "2244994945" }'Slack channels, members and identity
A Slack account connected through the app (not a webhook) has three extra resources:
GET /v1/accounts/{id}/slack/channels
GET /v1/accounts/{id}/slack/members
GET /v1/accounts/{id}/slack/identity
PATCH /v1/accounts/{id}/slack/identitychannels lists the channels the app can post to, with is_current on the one this account uses. members lists the people in the Slack workspace; a member's id is the handle for starting a DM. identity is the name and icon posts appear under:
{ "username": "Release Bot", "icon_url": "https://yourbrand.com/bot.png" }Omitted fields keep their value and null clears one. Set icon_url or icon_emoji (like :rocket:), not both. A webhook connection answers 409 webhook_connection. All four need the accounts scope.
Discord server
A Discord account connected as the bot (not a webhook) exposes the server it was installed into. Every route answers 409 webhook_connection on a webhook connection, and 502 when Discord itself does not answer.
Channels and the posting identity:
GET /v1/accounts/{id}/discord/channels
PATCH /v1/accounts/{id}/discord/channels/current
GET /v1/accounts/{id}/discord/identity
PATCH /v1/accounts/{id}/discord/identitychannels lists the server's text channels, each with can_post — false when a channel permission in Discord shuts the bot out — and is_current on the one this account uses. channels/current takes { "channel_id": "..." } and moves the account to another one in the same server; a channel the bot cannot post in answers 409 channel_not_writable. identity is the nickname and avatar the bot wears there:
{ "username": "Release Bot", "avatar_url": "https://yourbrand.com/bot.png" }Omitted fields keep their value and null clears one.
Messages, in the account's channel:
GET /v1/accounts/{id}/discord/messages/pinned
DELETE /v1/accounts/{id}/discord/messages/{messageId}
POST /v1/accounts/{id}/discord/messages/{messageId}/pin
DELETE /v1/accounts/{id}/discord/messages/{messageId}/pin
POST /v1/accounts/{id}/discord/messages/{messageId}/crosspost
POST /v1/accounts/{id}/discord/messages/{messageId}/thread
POST /v1/accounts/{id}/discord/dmcrosspost publishes an announcement-channel message to every server following the channel. thread takes { "name": "...", "auto_archive_duration": 1440 }. dm takes { "member_id": "...", "content": "..." } and sends one message to that member.
Scheduled events, members and roles:
GET /v1/accounts/{id}/discord/events
POST /v1/accounts/{id}/discord/events
GET /v1/accounts/{id}/discord/events/{eventId}
PATCH /v1/accounts/{id}/discord/events/{eventId}
DELETE /v1/accounts/{id}/discord/events/{eventId}
GET /v1/accounts/{id}/discord/members
GET /v1/accounts/{id}/discord/members/{memberId}
GET /v1/accounts/{id}/discord/roles
POST /v1/accounts/{id}/discord/roles
PATCH /v1/accounts/{id}/discord/roles/{roleId}
DELETE /v1/accounts/{id}/discord/roles/{roleId}
PUT /v1/accounts/{id}/discord/roles/{roleId}/members/{memberId}
DELETE /v1/accounts/{id}/discord/roles/{roleId}/members/{memberId}An event names either a channel_id (a voice or stage channel) or a location with an end_time:
{
"name": "Launch stream",
"start_time": "2026-10-01T18:00:00Z",
"end_time": "2026-10-01T19:00:00Z",
"location": "https://yourbrand.com/live"
}members takes q to search by username or nickname prefix, and limit. A member's id is the member_id for a DM and for a role assignment.
Everything here needs the accounts scope. The routes that put something on the server — deleting, pinning, unpinning, crossposting, starting a thread and sending a DM — need the publish scope as well.
Google Business Profile
A connected Google Business location is managed under gbp:
GET /v1/accounts/{id}/gbp/location
PATCH /v1/accounts/{id}/gbp/location
GET /v1/accounts/{id}/gbp/attributes
PATCH /v1/accounts/{id}/gbp/attributes
GET /v1/accounts/{id}/gbp/menus
PUT /v1/accounts/{id}/gbp/menus
GET /v1/accounts/{id}/gbp/services
PUT /v1/accounts/{id}/gbp/services
GET /v1/accounts/{id}/gbp/media
POST /v1/accounts/{id}/gbp/media
DELETE /v1/accounts/{id}/gbp/media/{mediaKey}
GET /v1/accounts/{id}/gbp/place-actions
POST /v1/accounts/{id}/gbp/place-actions
PATCH /v1/accounts/{id}/gbp/place-actions/{linkId}
DELETE /v1/accounts/{id}/gbp/place-actions/{linkId}
GET /v1/accounts/{id}/gbp/verification
POST /v1/accounts/{id}/gbp/verification/start
POST /v1/accounts/{id}/gbp/verification/complete
GET /v1/accounts/{id}/gbp/performance
POST /v1/accounts/{id}/gbp/assignReads need the accounts scope. Writes need publish as well, because they change what the listing shows to the public.
Responses relay Google's own shape, so a field you know from the Business Profile APIs is the field you get back. Requests are snake_case, and a PATCH changes only the fields it carries:
{ "title": "Corner Bakery", "primary_phone": "+1 555 0100" }menus and services are PUT rather than PATCH, because Google replaces the whole set — send the list you want to end up with.
A photo comes from your media library, never from a URL:
{ "media_id": "a3f1…", "category": "INTERIOR", "description": "Front counter" }The asset has to be in a workspace you can reach, and JPEG or PNG. Anything else is a 400.
performance takes start_date and end_date, and repeats daily_metrics for each metric you want. Pass keywords=true for the monthly search terms instead of the daily series. attributes takes available=true to list what Google offers for the location's category rather than what is set on it.
assign hands the location to another workspace you own, moving the connection and every row keyed to it in one transaction:
{ "workspace_id": "b7c2…" }Every route here answers 503 not_configured until Google grants this deployment Business Profile API access.
Next
Related documentation
- API Overview
Base URL, envelopes, pagination, and errors for the FoPost REST API.
- Authentication
API keys, scopes, and workspace binding.
- Publishing
Create a post, target accounts, publish it, and read the per-account result.
- Scheduling
Schedule a post, repeat it, and import a batch from a spreadsheet.
- Media
Upload files, list the media library, and attach media to a post.
- Validation
Check content, text length, and media against platform rules before a post exists.
- Blog
A keyword plan, articles written in the background, a review window, and a target they publish to.
- Blogs and Articles
Read and manage the articles and products that already live on a connected site.