|
Instant Cross-Posting
Write once, publish to all your platforms in seconds. No switching between apps.
Smart Scheduling
Schedule posts for the perfect time. Calendar view with drag-and-drop.
Secure by Default
All tokens encrypted with AES-256. Your accounts, your keys, your control.
Track Performance
Unified analytics across all platforms. See what resonates with your audience.
100% Free — X API fees passed through at cost
No subscriptions, no limits, no hidden fees. Open source and community-driven.
Sign in
Sign in to start cross-posting.
Check your email to verify your address and get started.
New post
Write once, publish everywhere.
Preview
0 platformsJuly 2026
Upcoming posts — cancel one before it goes out.
Upcoming posts
All scheduled posts, with any media that will be attached.
History
Published posts and per-platform results — a ✗ shows why a platform failed.
Teams
Organize your social accounts into teams (e.g. Personal, Business).
Connected accounts
API Reference
Post and schedule from your own apps.
API keys
Use these to post from your own apps.
Authentication
Generate an API key below, then send it as Authorization: Bearer fsp_live_… or FSP-API-KEY: fsp_live_…. Keys are scoped to your account and can be revoked anytime.
Posting
curl -X POST https://post.freesurf.tools/api/post \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platforms":["x","linkedin"],"text":"Hello from the API","team":"Personal"}'
await fetch("https://post.freesurf.tools/api/post", {
method: "POST",
headers: {
"Authorization": "Bearer fsp_live_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ platforms: ["x"], text: "Hello from JS", team: "Personal" })
});
import requests
requests.post("https://post.freesurf.tools/api/post",
headers={"Authorization": "Bearer fsp_live_..."},
json={"platforms": ["x"], "text": "Hello from Python", "team": "Personal"})
Body: platforms (array), text, team (name) or teamId (id) — one is required — plus optional mediaUrls.
Instagram feed images auto-fit by default; pass "instagramImageFit":"crop" to center-crop instead. For Facebook, LinkedIn, and YouTube, select a page/channel in the Accounts tab so posts go to the right target.
Response:
{
"id": "post-uuid",
"results": [
{ "platform": "x", "success": true, "postUrl": "https://x.com/..." }
],
"postedAt": "2026-08-30T19:00:00Z"
}
Media (images / videos) — upload a local file to get an uploadId, then pass it in mediaUrls:
curl -X POST https://post.freesurf.tools/api/media/upload \
-H "Authorization: Bearer fsp_live_..." \
-F "file=@/path/to/image.png"
# then post with the returned uploadId
curl -X POST https://post.freesurf.tools/api/post \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platforms":["x"],"text":"Caption here","team":"Personal","mediaUrls":["UPLOAD_ID"]}'
The upload response is JSON — grab the uploadId field (it's the first field) and use it in mediaUrls. PowerShell one-liner to capture it:
$uploadId = (curl.exe -s -X POST https://post.freesurf.tools/api/media/upload -H "FSP-API-KEY: fsp_live_..." -F "file=@video.mp4" | ConvertFrom-Json).uploadId
text is the caption. Pass multiple ids for carousels (X, LinkedIn, Instagram); TikTok/YouTube need a single video.
Supported types: jpg / jpeg / png / gif / mp4 / mov / pdf.
Scheduling
curl -X POST https://post.freesurf.tools/api/schedule \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platforms":["x"],"text":"Later post","scheduledAt":"2026-08-31T14:00:00Z","team":"Personal"}'
scheduledAt must be a future ISO timestamp. Team (by name or teamId) is required, same as posting. Posts publish within ~5 minutes of that time.
List / cancel scheduled posts
curl https://post.freesurf.tools/api/scheduled \
-H "Authorization: Bearer fsp_live_..."
curl -X DELETE https://post.freesurf.tools/api/scheduled/POST_ID \
-H "Authorization: Bearer fsp_live_..."
Teams
Post to a team by name (team) or id (teamId) — either works. Team names are unique per account:
# By team name
curl -X POST https://post.freesurf.tools/api/post \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platforms":["x"],"text":"For my business","team":"Personal"}'
# By team id (get ids from /api/teams)
curl -X POST https://post.freesurf.tools/api/post \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platforms":["x"],"text":"For my business","teamId":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}'
A team is required for every post — posts never default to another team.
Delete a published post
Developer / API only — irreversible, so it's intentionally not exposed in the UI. Deletes a post already published to a platform (currently X). X deletes are metered at $0.01 each, drawn from your credit balance.
curl -X POST https://post.freesurf.tools/api/posts/delete \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platform":"x","postId":""}'
postId is the recorded postId from GET /api/posts/recent for that platform's result. There's no undo.
Import post history
Backfill posts a connected account published before it was connected (runs as an async job) — useful for X, whose per-post analytics aren't served passively. Imported posts come back with their analytics (impressions / likes / comments / shares where the platform reports them). Supports x, bluesky, threads, tiktok, linkedin, instagram, facebook, pinterest, youtube, reddit, mastodon.
# Start an import (async job — runs in the background)
curl -X POST https://post.freesurf.tools/api/import \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platform":"x","count":50,"withAnalytics":true}'
# Poll job status (queued / running / completed / failed)
curl "https://post.freesurf.tools/api/imports?platform=x" \
-H "Authorization: Bearer fsp_live_..."
# List the imported posts with their analytics + remaining capacity
curl "https://post.freesurf.tools/api/imports/posts?platform=x&count=50" \
-H "Authorization: Bearer fsp_live_..."
count defaults to 50. Monthly import caps apply (e.g. 100/month on Pro); the posts endpoint returns remainingCapacity. Imports are billed at $0.005 per result. Refresh still only pulls live metrics for posts this app published — use import for anything that came before.
Comments & replies
Pull the comments on one of your posts, then reply straight to a specific comment. Replying posts live and is metered like a post — $0.015 plain, $0.20 with a link — drawn from your credit balance.
# 1) Import the comments for a post (async — poll /api/comments until they appear)
curl -X POST https://post.freesurf.tools/api/comments/import \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platform":"x","postId":""}'
# 2) List the fetched comments (each has an id, authorName, text, publishedAt)
curl "https://post.freesurf.tools/api/comments?platform=x&postId=" \
-H "Authorization: Bearer fsp_live_..."
# 3) Reply to one of those comments (commentId = the comment's id from step 2)
curl -X POST https://post.freesurf.tools/api/reply \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"platform":"x","postId":"","commentId":"","text":"Thanks for the note!"}'
postId is the recorded postId of the original post (its per-platform result from GET /api/posts/recent). Works on X and Bluesky today.
Supported platforms
Platform keys you can pass in platforms:
Key Platform
-------- -----------
x X / Twitter
linkedin LinkedIn
facebook Facebook
instagram Instagram
threads Threads
tiktok TikTok
youtube YouTube
bluesky Bluesky
reddit Reddit
pinterest Pinterest
slack Slack
discord Discord
google_business Google Business
Reddit, Pinterest, Slack, Discord, and Google Business can be connected in Accounts. When posting to them, pass the required target: platformTargets = {"discord":"channelId","slack":"channelId","pinterest":"boardName","reddit":"r/example"}.
X fees & billing
| Action | Cost |
|---|---|
| Post | $0.015 |
| Post with a link | $0.20 |
| Comment / reply | $0.015 |
| Comment / reply with a link | $0.20 |
| Delete | $0.01 |
| Post analytics (per result) | $0.005 |
| Comment import (per result) | $0.005 |
| Post history import (per result) | $0.005 |
Source: X (metered at cost)
Credits endpoints:
curl https://post.freesurf.tools/api/credits \
-H "Authorization: Bearer fsp_live_..."
curl -X POST https://post.freesurf.tools/api/credits/topup \
-H "Authorization: Bearer fsp_live_..." \
-H "Content-Type: application/json" \
-d '{"amountCents":500}' # returns a Stripe Checkout URL
Rate limits
Daily limits per social account.
| Platform | PRO |
|---|---|
| Twitter / X | 15 |
| 50 | |
| 50 | |
| 18 | |
| YouTube | 10 |
| TikTok | 10 |
| Threads | 200 |
| 24 | |
| 24 | |
| Discord | 100 |
| Slack | 100 |
| Mastodon | 50 |
| Bluesky | 50 |
| Google Business | 20 |
| Snapchat | 20 |
Source: daily limits per connected account.
Common errors
{"error":"Invalid JSON body"} (400) — usually a client-side quoting problem. PowerShell 5.1 corrupts embedded quotes when you pass -d as an argument; pipe the body through stdin instead (-d "@-") or write it to a file (-d @body.json). Keep captions ASCII — emoji/non-ASCII in a PowerShell -d argument also mangles the JSON.
{"error":"teamId or team is required"} (400) — every post and schedule needs a team, by name ("team": "Personal") or id ("teamId": "..."). Get ids from GET /api/teams.
401 Unauthorized — missing, revoked, or wrong-position API key. Send it as Authorization: Bearer fsp_live_... or FSP-API-KEY: fsp_live_....
{"error":"Unsupported file type"} (400) on upload — the file extension isn't in the supported list (jpg/jpeg/png/gif/mp4/mov/pdf). The type is inferred from the filename, so name the file with the right extension.
X: Insufficient X credit — X is metered ($0.015 plain, $0.20 with a link). Top up in the X fees tab.
Comment create: Request Validation Error (400) — a required field is missing (commonly the post id). The error message names the missing field.
{"error":"Unknown platform"} (400) — the platform key isn't supported (see the platform list above).
Drafts & Content Library
Save and reuse your content ideas.
No drafts yet. Save a post to get started.
Hashtag Manager
Organize hashtag groups for different platforms and topics.
No hashtag groups yet. Create one to get started.
Comments
All comments in one place.
Comments are coming soon.
Automated replies
Set automatic responses to common comments.
Posts
Metrics aren't pulled automatically — some platforms (like X) charge per pull (~$0.005 per X post). Click Refresh to pull the posts shown here (load more first to refresh more).
Engagement per post, broken out by platform. New posts can take time to show engagement.
X fees
When you top up on web, the Stripe managed payments fee is deducted from what you add — e.g. a $10 top-up becomes $9.00 in credit for X after the 6.5% + $0.35 fee. Stripe handles any sales tax at checkout on their end — it isn't taken out of your credits. Store purchases (Apple/Google) deduct their 15–30% the same way. X charges $0.015 for a plain/media post and $0.20 for a post containing a link.
Transactions
X usage billing
| Action | Cost |
|---|---|
| Post | $0.015 |
| Post with a link | $0.20 |
| Comment / reply | $0.015 |
| Comment / reply with a link | $0.20 |
| Delete | $0.01 |
| Post analytics (per result) | $0.005 |
| Comment import (per result) | $0.005 |
| Post history import (per result) | $0.005 |
Source: X (metered at cost)
Transaction fees
| Fee | Cost |
|---|---|
| Stripe managed payments (per top-up transaction) | 6.5% + $0.35 |
| Apple App Store in-app purchase | 15–30% |
| Google Play in-app purchase | 15–30% |
Stripe and store fees apply on top of the X posting cost. Standard published rates.