API & MCP reference
One REST API to publish and schedule across all nine platforms Synapse supports — or connect it to any AI client over MCP and do it in plain English.
Quick start
Every request goes to https://withsynapse.app/api/v1 and carries your API key. Three steps to your first post:
- Create a key on the API Keys page. It starts with
syn_and is shown once. - Find out what you can post to with
GET /accounts. - Create the post.
# What can I post to?
curl https://withsynapse.app/api/v1/accounts \
-H "Authorization: Bearer syn_your_key"
# Publish to every connected LinkedIn and Bluesky account
curl -X POST https://withsynapse.app/api/v1/posts \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{
"content": "Hello from the Synapse API.",
"platforms": ["LinkedIn", "Bluesky"],
"publish": true
}'Leave out publish to save a draft, or send scheduled_time to schedule it — Synapse publishes scheduled posts for you.
Authentication
Send your key as a bearer token on every request. Keys are stored hashed, so a lost key cannot be recovered — create a new one and revoke the old one.
Authorization: Bearer syn_your_keyYour key carries the same power as your account: it can publish to your connected accounts and delete your posts. Keep it server-side, never commit it, and never ship it in a browser or mobile app. You can hold up to 25 keys and revoke any of them instantly from the API Keys page.
Rate limits
Limits are per account, per minute. A limited request returns 429 with a Retry-After header telling you how many seconds to wait.
| Action | Limit |
|---|---|
| Reading (list or fetch) | 60 requests / minute |
| Creating & editing posts | 20 requests / minute |
| Uploading media | 30 requests / minute |
| Publishing | 10 requests / minute |
Errors
Errors return a JSON body with a single error field written to be read by a human, and the matching HTTP status.
{
"error": "`scheduled_time` must include an explicit timezone, e.g. 2026-08-01T09:00:00Z or 2026-08-01T17:00:00+08:00."
}| Status | Meaning |
|---|---|
| 200 OK | The request succeeded. |
| 201 Created | The post or media record was created. |
| 400 Bad Request | Something in your input is invalid. The `error` field says exactly what. |
| 401 Unauthorized | Missing, malformed or revoked API key. |
| 402 Payment Required | The key's account has no active trial or subscription. |
| 404 Not Found | No post or media with that id on your account. |
| 409 Conflict | The post is already publishing, or changed while you were editing it. |
| 429 Too Many Requests | Rate limited. Wait for the seconds in the Retry-After header. |
| 500 Server Error | Something went wrong on our side. Safe to retry. |
Core concepts
A post targets accounts, not platforms. Naming a platform is shorthand for “every account I have connected there”. If you have two Instagram accounts, "platforms": ["Instagram"] posts to both; use accounts with ids from GET /accounts to pick one.
Each account succeeds or fails on its own. One platform rejecting your post never blocks the others, and every account gets its own result with a live URL or an error message. That is why a post can end up partially_posted — and why retrying only re-attempts the failures.
Nothing is ever published twice. An account already recorded as posted is skipped on every subsequent publish or retry, and a post that is mid-publish rejects a second concurrent attempt with a 409.
Post statuses
| Status | Meaning |
|---|---|
| draft | Saved but not scheduled and not published. |
| scheduled | Waiting for its scheduled_time. Published automatically. |
| publishing | A publish attempt is in flight right now. |
| posted | Every targeted account published successfully. |
| partially_posted | Some accounts succeeded, others failed. Retryable. |
| failed | Every targeted account failed. Retryable. |
Platform names
These strings are exact — including spacing and punctuation. Anything else is rejected with a list of the valid names.
List connected accounts
Every social account connected to your Synapse account, with the ids and exact platform names to use elsewhere. Start here — the platform names are case- and punctuation-sensitive.
curl https://withsynapse.app/api/v1/accounts \
-H "Authorization: Bearer syn_your_key"{
"accounts": [
{
"platform": "LinkedIn",
"account_id": "abc123",
"username": "Ayden Lim",
"connected_at": "2026-07-02T09:14:22.000Z"
},
{
"platform": "Bluesky",
"account_id": "you.bsky.social",
"username": "you.bsky.social",
"connected_at": "2026-07-04T11:02:10.000Z"
}
],
"supported_platforms": ["Twitter / X", "LinkedIn", "Bluesky", "TikTok",
"Instagram", "YouTube", "Facebook", "Threads", "Pinterest"]
}List posts
Your posts, newest first, each with the outcome for every account it targeted.
Query parameters
| Field | Type | Description |
|---|---|---|
| status | string | Filter by status (see Post statuses). |
| limit | integer | How many to return, 1–100. Defaults to 25. |
| offset | integer | Skip this many results, for paging. |
curl "https://withsynapse.app/api/v1/posts?status=failed&limit=10" \
-H "Authorization: Bearer syn_your_key"{
"posts": [
{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"content": "Shipping day",
"status": "partially_posted",
"scheduled_time": null,
"created_at": "2026-07-29T08:00:00.000Z",
"platforms": ["LinkedIn", "Bluesky"],
"accounts": [
{
"platform": "LinkedIn",
"account_id": "abc123",
"username": "Ayden Lim",
"result": {
"status": "posted",
"url": "https://www.linkedin.com/feed/update/...",
"postedAt": "2026-07-29T08:00:04.000Z"
}
},
{
"platform": "Bluesky",
"account_id": "you.bsky.social",
"username": "you.bsky.social",
"result": {
"status": "failed",
"error": "Bluesky account isn't connected — reconnect it.",
"postedAt": null
}
}
],
"captions": {},
"media": [],
"config": {}
}
]
}Create a post
Creates a draft, schedules a post, or publishes immediately. Target either `platforms` (every connected account on those platforms) or `accounts` (specific ids). If `publish` is true the response waits for the result and includes it.
Body parameters
| Field | Type | Description |
|---|---|---|
| content | string | The caption. Required unless you attach media. |
| platforms | string[] | Platform names to post to. |
| accounts | string[] | Specific account ids. Takes precedence over platforms. |
| media | string[] | Media paths from the upload flow. Up to 10. |
| captions | object | Per-platform caption overrides, keyed by platform name. |
| config | object | Per-platform publishing options. See the config reference. |
| scheduled_time | string | ISO 8601 with a timezone offset. Makes the post scheduled. |
| publish | boolean | Publish immediately instead of saving a draft. |
curl -X POST https://withsynapse.app/api/v1/posts \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{
"content": "Shipping day. Synapse 1.1 is live.",
"platforms": ["LinkedIn", "Twitter / X"],
"captions": {
"Twitter / X": "Synapse 1.1 is live."
},
"config": {
"linkedin": { "visibility": "PUBLIC" },
"firstComment": "Full changelog in the replies."
},
"publish": true
}'{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"content": "Shipping day. Synapse 1.1 is live.",
"status": "posted",
"scheduled_time": null,
"created_at": "2026-07-29T08:00:00.000Z",
"platforms": ["LinkedIn", "Twitter / X"],
"accounts": [
{
"platform": "LinkedIn",
"account_id": "abc123",
"username": "Ayden Lim",
"result": {
"status": "posted",
"url": "https://www.linkedin.com/feed/update/...",
"postedAt": "2026-07-29T08:00:04.000Z"
}
}
],
"captions": { "Twitter / X": "Synapse 1.1 is live." },
"media": [],
"config": { "linkedin": { "visibility": "PUBLIC" } }
}Retrieve a post
One post, including the live URL or the error message for each account.
curl https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11 \
-H "Authorization: Bearer syn_your_key"{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"status": "posted",
"content": "Shipping day.",
"accounts": [ /* ... one entry per targeted account ... */ ],
"captions": {},
"media": [],
"config": {},
"scheduled_time": null,
"created_at": "2026-07-29T08:00:00.000Z",
"platforms": ["LinkedIn"]
}Update a post
Edit a draft, scheduled or failed post. Only the fields you send are changed. Send `scheduled_time: null` to unschedule it back to a draft. Published posts cannot be edited — editing our record would not change what is already live, so the request returns 409.
Body parameters
| Field | Type | Description |
|---|---|---|
| content | string | Replace the caption. |
| platforms | string[] | Replace the target platforms. |
| accounts | string[] | Replace the target accounts. |
| media | string[] | Replace the attached media. |
| captions | object | Replace the per-platform overrides. |
| config | object | Replace the publishing options. |
| scheduled_time | string | null | Reschedule, or null to unschedule. |
curl -X PATCH https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11 \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{
"content": "Shipping next week instead.",
"scheduled_time": "2026-08-03T09:00:00+08:00"
}'{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"content": "Shipping next week instead.",
"status": "scheduled",
"scheduled_time": "2026-08-03T01:00:00.000Z",
"platforms": ["LinkedIn"],
"accounts": [ /* ... */ ],
"captions": {},
"media": [],
"config": {},
"created_at": "2026-07-29T08:00:00.000Z"
}Publish or retry a post
Publishes an existing post right now. By default every account that has not already succeeded is attempted; an account already recorded as posted is never published twice. Use `mode: "retry"` after a partial failure to re-attempt only the accounts that failed.
Body parameters
| Field | Type | Description |
|---|---|---|
| mode | "auto" | "retry" | Defaults to "auto". Use "retry" for failures only. |
curl -X POST https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11/publish \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{ "mode": "retry" }'{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"status": "posted",
"accounts": [
{
"platform": "Bluesky",
"account_id": "you.bsky.social",
"username": "you.bsky.social",
"result": {
"status": "posted",
"url": "https://bsky.app/profile/you.bsky.social/post/3k...",
"postedAt": "2026-07-29T09:12:00.000Z"
}
}
],
"content": "Shipping day.",
"captions": {},
"media": [],
"config": {},
"scheduled_time": null,
"created_at": "2026-07-29T08:00:00.000Z",
"platforms": ["Bluesky"]
}Delete a post
Removes the post from Synapse. Where the platform's API supports deletion, the live copy is deleted too. This cannot be undone.
curl -X DELETE https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11 \
-H "Authorization: Bearer syn_your_key"{
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"deleted": true
}List media
Files in your media library, each with the `path` to reuse when creating a post and a temporary preview URL.
Query parameters
| Field | Type | Description |
|---|---|---|
| limit | integer | How many to return, 1–100. Defaults to 50. |
curl https://withsynapse.app/api/v1/media \
-H "Authorization: Bearer syn_your_key"{
"media": [
{
"id": "3c7a...",
"path": "…/1753790000-launch.png",
"type": "image",
"filename": "launch.png",
"created_at": "2026-07-29T07:55:00.000Z",
"url": "https://…"
}
]
}Start an upload
Returns a short-lived URL to upload your file to directly. Because the bytes never pass through this API there is no request-size limit — large videos are fine. The URL expires shortly and works for exactly one file.
Body parameters
| Field | Type | Description |
|---|---|---|
| filenamerequired | string | The file's name, e.g. "launch.png". |
| content_typerequired | string | One of image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime, video/webm. |
curl -X POST https://withsynapse.app/api/v1/media \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{ "filename": "launch.png", "content_type": "image/png" }'{
"path": "…/1753790000-launch.png",
"upload_url": "https://…",
"instructions": {
"method": "PUT",
"headers": { "Content-Type": "image/png" },
"then": "POST /api/v1/media/complete with { path, filename, content_type }"
}
}Finish an upload
Records the uploaded file in your media library and returns the `path` to pass to `media` when creating a post. Call this after the file has finished uploading.
Body parameters
| Field | Type | Description |
|---|---|---|
| pathrequired | string | The `path` returned when you started the upload. |
| filename | string | Display name. Defaults to the file's name. |
| content_type | string | Used to classify the file as an image or a video. |
curl -X POST https://withsynapse.app/api/v1/media/complete \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{
"path": "…/1753790000-launch.png",
"filename": "launch.png",
"content_type": "image/png"
}'{
"id": "3c7a...",
"path": "…/1753790000-launch.png",
"type": "image",
"filename": "launch.png",
"created_at": "2026-07-29T07:55:00.000Z",
"url": "https://…"
}Uploading media
Uploads happen in three steps so your file goes straight to storage instead of through this API — which means there is no size limit, and a several-hundred-megabyte video works fine. Ask for an upload URL, PUT the bytes to it, then record the result and use the returned path in media when you create a post.
# 1. Ask for an upload URL
RESP=$(curl -s -X POST https://withsynapse.app/api/v1/media \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d '{ "filename": "launch.png", "content_type": "image/png" }')
PATH_=$(echo "$RESP" | jq -r .path)
URL=$(echo "$RESP" | jq -r .upload_url)
# 2. Upload the bytes straight to storage (no size limit)
curl -X PUT "$URL" -H "Content-Type: image/png" --data-binary @launch.png
# 3. Record it, then use the path in a post
curl -s -X POST https://withsynapse.app/api/v1/media/complete \
-H "Authorization: Bearer syn_your_key" \
-H "Content-Type: application/json" \
-d "{\"path\":\"$PATH_\",\"filename\":\"launch.png\",\"content_type\":\"image/png\"}"Accepted types are JPEG, PNG, WebP and GIF images, and MP4, MOV and WebM video. A post can carry up to 10 files.
Config reference
The config object reaches every per-platform option the Synapse composer exposes. Every key is optional, and unknown keys are rejected so a typo never fails silently.
TikTok posts are private by default. If you do not set config.tiktok.privacy_level, the post publishes as SELF_ONLY — visible only to you. Set it to PUBLIC_TO_EVERYONE to publish publicly.
{
"config": {
"tiktok": { "privacy_level": "PUBLIC_TO_EVERYONE", "allow_duet": false },
"youtube": { "title": "Synapse in 60 seconds", "privacy": "public" },
"linkedin": { "visibility": "PUBLIC" },
"cover": "…/1753790000-thumbnail.png",
"firstComment": "Full changelog in the replies."
}
}Applies to every platform
| Field | Type | Description |
|---|---|---|
| firstComment | string | Posted as the first comment right after publishing. Supported on Bluesky and LinkedIn. |
| cover | string | A media path to use as the video cover image (YouTube thumbnail, Reel cover, Pinterest video cover). |
| contentType | "story" | "shorts" | Publish to Instagram/Facebook as a 24-hour Story, or as short-form vertical video. |
YouTube config.youtube
| Field | Type | Description |
|---|---|---|
| title | string | Video title. Defaults to the caption's first line. |
| tags | string[] | Search keywords (up to 30). |
| privacy | "public" | "unlisted" | "private" | Visibility of the upload. |
| categoryId | string | YouTube category id, e.g. "22" for People & Blogs. |
| playlistId | string | Add the upload to this playlist. |
| madeForKids | boolean | Audience flag required by COPPA. |
TikTok config.tiktok
| Field | Type | Description |
|---|---|---|
| privacy_level | "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "SELF_ONLY" | Defaults to SELF_ONLY (private). Set this explicitly to publish publicly. |
| allow_comment | boolean | Allow comments on the post. |
| allow_duet | boolean | Allow Duets (video only). |
| allow_stitch | boolean | Allow Stitches (video only). |
| auto_add_music | boolean | Let TikTok add a recommended song (photo posts only). |
| disclose_content | boolean | Turn on commercial-content disclosure. |
| brand_organic | boolean | Promoting your own brand. |
| brand_content | boolean | Paid partnership. Cannot be combined with SELF_ONLY. |
| is_aigc | boolean | Label the post as AI-generated. |
Pinterest config.pinterest
| Field | Type | Description |
|---|---|---|
| title | string | Pin title. Defaults to the caption's first line. |
| description | string | Pin description. Defaults to the caption. |
| link | string | Destination link for the Pin. |
| boardId | string | Which board to pin to. Defaults to your first board. |
Instagram config.instagram
| Field | Type | Description |
|---|---|---|
| locationId | string | Tagged location id. |
| collaborators | string[] | Usernames invited as collaborators (up to 10). |
| commentsEnabled | boolean | Set false to disable comments. |
Facebook config.facebook
| Field | Type | Description |
|---|---|---|
| asReel | boolean | Publish a short vertical video as a Reel. |
| countries | string[] | Restrict visibility to these ISO-2 country codes. |
LinkedIn config.linkedin
| Field | Type | Description |
|---|---|---|
| visibility | "PUBLIC" | "CONNECTIONS" | Who can see the post. |
Threads config.threads
| Field | Type | Description |
|---|---|---|
| topicTag | string | A single topic tag for the post. |
Bluesky config.bluesky
| Field | Type | Description |
|---|---|---|
| replyControl | "everybody" | "nobody" | "custom" | Who may reply. |
| replyFollowers | boolean | Custom mode: your followers may reply. |
| replyFollowing | boolean | Custom mode: people you follow may reply. |
| replyMentioned | boolean | Custom mode: mentioned users may reply. |
| allowQuotes | boolean | Set false to disable quote posts. |
Platform rules
Caption limits are enforced before anything is sent, counted the way each platform counts — X charges 23 characters for any link regardless of its real length. Per-platform overrides in captionsare checked against their own platform’s limit.
| Platform | Caption limit | What it accepts |
|---|---|---|
| Twitter / X | 280 | Text, images or one video. Links count as 23 characters. |
| 3,000 | Text, images, or one video. | |
| Bluesky | 300 | Text, up to 4 images, or one video (not both). |
| TikTok | 2,200 | Requires a video or at least one image. |
| 2,200 | Requires media: image, carousel, Reel or Story. | |
| YouTube | 5,000 | Requires a video. |
| 63,206 | Text, photos, or one video. | |
| Threads | 500 | Text, or one image, or one video. |
| 500 | Requires an image or video. Video Pins need a cover. |
Webhooks
Add a webhook URL on the API Keys page and Synapse will POST there whenever a post finishes publishing — however it was created, including scheduled posts published while you were asleep. The URL must be HTTPS.
{
"event": "post.completed",
"timestamp": "2026-07-29T08:00:05.000Z",
"post": {
"id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
"status": "partially_posted",
"results": {
"abc123": {
"status": "posted",
"url": "https://www.linkedin.com/feed/update/...",
"postedAt": "2026-07-29T08:00:04.000Z"
},
"you.bsky.social": {
"status": "failed",
"error": "Bluesky account isn't connected — reconnect it.",
"postedAt": null
}
}
}
}Every delivery is signed with your webhook secret in the X-Synapse-Signature header, as sha256=<hex> over the raw request body. Verify it before trusting the payload, and always compare in constant time.
const crypto = require("crypto");
// Your signing secret is shown on the API Keys page.
function verify(rawBody, signatureHeader, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// Constant-time compare — never use ===, it leaks the secret byte by byte.
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/synapse-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.get("X-Synapse-Signature"), process.env.SYNAPSE_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString());
console.log(event.post.status);
res.sendStatus(200);
});Deliveries time out after 5 seconds and are not retried, so treat the webhook as a fast notification: acknowledge it immediately and do your work afterwards. It is a convenience, not a guarantee — GET /posts/{id} is always the source of truth.
MCP server
Synapse ships an official MCP server, so any MCP-capable AI client — Claude Desktop, Cursor and others — can post for you in plain English. It runs locally with your own API key; there is nothing to host and no extra cost.
Add this to your client’s MCP configuration. In Claude Desktop that is Settings → Developer → Edit Config. Nothing to install first — npx fetches it on demand.
{
"mcpServers": {
"synapse": {
"command": "npx",
"args": ["-y", "synapse-social-mcp"],
"env": {
"SYNAPSE_API_KEY": "syn_your_key_here"
}
}
}
}Available tools
| Tool | What it does |
|---|---|
| list_accounts | Your connected accounts and the exact platform names to use. |
| list_posts | Recent posts, filterable by status. |
| get_post | One post, with the live URL or error for each account. |
| create_post | Draft, schedule or publish now — with media, captions and options. |
| update_post | Edit a draft, scheduled or failed post. |
| publish_post | Publish now, or retry only the accounts that failed. |
| delete_post | Delete a post, and the live copy where supported. |
| list_media | Files already in your media library. |
| upload_media | Upload a local image or video by file path. |
Because the server runs on your own machine it can read local files: hand it a path and it uploads the image or video for you.
Things to try
- Which social accounts do I have connected to Synapse?
- Post this to Instagram and X: “Shipping day” with the image at ~/Desktop/launch.png
- Schedule a LinkedIn post for Monday 9am Singapore time: “5 lessons from shipping in public”.
- Post my demo video to TikTok and YouTube — make TikTok public and title the YouTube one “Synapse in 60 seconds”.
- Did anything fail to post this week? Retry just those.