synapse logosynapse

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.

Create an API keyMCP server on npm
On this page
  • Quick start
  • Authentication
  • Rate limits
  • Errors
  • Core concepts
  • List connected accounts
  • List posts
  • Create a post
  • Retrieve a post
  • Update a post
  • Publish or retry a post
  • Delete a post
  • List media
  • Start an upload
  • Finish an upload
  • Uploading media
  • Config reference
  • Platform rules
  • Webhooks
  • MCP server

On this page

  • Quick start
  • Authentication
  • Rate limits
  • Errors
  • Core concepts
  • List connected accounts
  • List posts
  • Create a post
  • Retrieve a post
  • Update a post
  • Publish or retry a post
  • Delete a post
  • List media
  • Start an upload
  • Finish an upload
  • Uploading media
  • Config reference
  • Platform rules
  • Webhooks
  • MCP server

Quick start

Every request goes to https://withsynapse.app/api/v1 and carries your API key. Three steps to your first post:

  1. Create a key on the API Keys page. It starts with syn_ and is shown once.
  2. Find out what you can post to with GET /accounts.
  3. Create the post.
Your first 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.

Header
Authorization: Bearer syn_your_key

Your 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.

ActionLimit
Reading (list or fetch)60 requests / minute
Creating & editing posts20 requests / minute
Uploading media30 requests / minute
Publishing10 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 response
{
  "error": "`scheduled_time` must include an explicit timezone, e.g. 2026-08-01T09:00:00Z or 2026-08-01T17:00:00+08:00."
}
StatusMeaning
200 OKThe request succeeded.
201 CreatedThe post or media record was created.
400 Bad RequestSomething in your input is invalid. The `error` field says exactly what.
401 UnauthorizedMissing, malformed or revoked API key.
402 Payment RequiredThe key's account has no active trial or subscription.
404 Not FoundNo post or media with that id on your account.
409 ConflictThe post is already publishing, or changed while you were editing it.
429 Too Many RequestsRate limited. Wait for the seconds in the Retry-After header.
500 Server ErrorSomething 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

StatusMeaning
draftSaved but not scheduled and not published.
scheduledWaiting for its scheduled_time. Published automatically.
publishingA publish attempt is in flight right now.
postedEvery targeted account published successfully.
partially_postedSome accounts succeeded, others failed. Retryable.
failedEvery 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.

Twitter / XLinkedInBlueskyTikTokInstagramYouTubeFacebookThreadsPinterest

List connected accounts

GET/api/v1/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.

Request
curl https://withsynapse.app/api/v1/accounts \
  -H "Authorization: Bearer syn_your_key"
Response
{
  "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

GET/api/v1/posts

Your posts, newest first, each with the outcome for every account it targeted.

Query parameters

FieldTypeDescription
statusstringFilter by status (see Post statuses).
limitintegerHow many to return, 1–100. Defaults to 25.
offsetintegerSkip this many results, for paging.
Request
curl "https://withsynapse.app/api/v1/posts?status=failed&limit=10" \
  -H "Authorization: Bearer syn_your_key"
Response
{
  "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

POST/api/v1/posts

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

FieldTypeDescription
contentstringThe caption. Required unless you attach media.
platformsstring[]Platform names to post to.
accountsstring[]Specific account ids. Takes precedence over platforms.
mediastring[]Media paths from the upload flow. Up to 10.
captionsobjectPer-platform caption overrides, keyed by platform name.
configobjectPer-platform publishing options. See the config reference.
scheduled_timestringISO 8601 with a timezone offset. Makes the post scheduled.
publishbooleanPublish immediately instead of saving a draft.
Request
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
  }'
Response
{
  "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

GET/api/v1/posts/{id}

One post, including the live URL or the error message for each account.

Request
curl https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11 \
  -H "Authorization: Bearer syn_your_key"
Response
{
  "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

PATCH/api/v1/posts/{id}

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

FieldTypeDescription
contentstringReplace the caption.
platformsstring[]Replace the target platforms.
accountsstring[]Replace the target accounts.
mediastring[]Replace the attached media.
captionsobjectReplace the per-platform overrides.
configobjectReplace the publishing options.
scheduled_timestring | nullReschedule, or null to unschedule.
Request
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"
  }'
Response
{
  "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

POST/api/v1/posts/{id}/publish

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

FieldTypeDescription
mode"auto" | "retry"Defaults to "auto". Use "retry" for failures only.
Request
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" }'
Response
{
  "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

DELETE/api/v1/posts/{id}

Removes the post from Synapse. Where the platform's API supports deletion, the live copy is deleted too. This cannot be undone.

Request
curl -X DELETE https://withsynapse.app/api/v1/posts/9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11 \
  -H "Authorization: Bearer syn_your_key"
Response
{
  "id": "9f1c2e64-1a3b-4c8d-9e77-0b2f5a6d3c11",
  "deleted": true
}

List media

GET/api/v1/media

Files in your media library, each with the `path` to reuse when creating a post and a temporary preview URL.

Query parameters

FieldTypeDescription
limitintegerHow many to return, 1–100. Defaults to 50.
Request
curl https://withsynapse.app/api/v1/media \
  -H "Authorization: Bearer syn_your_key"
Response
{
  "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

POST/api/v1/media

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

FieldTypeDescription
filenamerequiredstringThe file's name, e.g. "launch.png".
content_typerequiredstringOne of image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime, video/webm.
Request
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" }'
Response
{
  "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

POST/api/v1/media/complete

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

FieldTypeDescription
pathrequiredstringThe `path` returned when you started the upload.
filenamestringDisplay name. Defaults to the file's name.
content_typestringUsed to classify the file as an image or a video.
Request
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"
  }'
Response
{
  "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.

Full upload, end to end
# 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 example
{
  "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

FieldTypeDescription
firstCommentstringPosted as the first comment right after publishing. Supported on Bluesky and LinkedIn.
coverstringA 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

FieldTypeDescription
titlestringVideo title. Defaults to the caption's first line.
tagsstring[]Search keywords (up to 30).
privacy"public" | "unlisted" | "private"Visibility of the upload.
categoryIdstringYouTube category id, e.g. "22" for People & Blogs.
playlistIdstringAdd the upload to this playlist.
madeForKidsbooleanAudience flag required by COPPA.

TikTok config.tiktok

FieldTypeDescription
privacy_level"PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "SELF_ONLY"Defaults to SELF_ONLY (private). Set this explicitly to publish publicly.
allow_commentbooleanAllow comments on the post.
allow_duetbooleanAllow Duets (video only).
allow_stitchbooleanAllow Stitches (video only).
auto_add_musicbooleanLet TikTok add a recommended song (photo posts only).
disclose_contentbooleanTurn on commercial-content disclosure.
brand_organicbooleanPromoting your own brand.
brand_contentbooleanPaid partnership. Cannot be combined with SELF_ONLY.
is_aigcbooleanLabel the post as AI-generated.

Pinterest config.pinterest

FieldTypeDescription
titlestringPin title. Defaults to the caption's first line.
descriptionstringPin description. Defaults to the caption.
linkstringDestination link for the Pin.
boardIdstringWhich board to pin to. Defaults to your first board.

Instagram config.instagram

FieldTypeDescription
locationIdstringTagged location id.
collaboratorsstring[]Usernames invited as collaborators (up to 10).
commentsEnabledbooleanSet false to disable comments.

Facebook config.facebook

FieldTypeDescription
asReelbooleanPublish a short vertical video as a Reel.
countriesstring[]Restrict visibility to these ISO-2 country codes.

LinkedIn config.linkedin

FieldTypeDescription
visibility"PUBLIC" | "CONNECTIONS"Who can see the post.

Threads config.threads

FieldTypeDescription
topicTagstringA single topic tag for the post.

Bluesky config.bluesky

FieldTypeDescription
replyControl"everybody" | "nobody" | "custom"Who may reply.
replyFollowersbooleanCustom mode: your followers may reply.
replyFollowingbooleanCustom mode: people you follow may reply.
replyMentionedbooleanCustom mode: mentioned users may reply.
allowQuotesbooleanSet 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.

PlatformCaption limitWhat it accepts
Twitter / X280Text, images or one video. Links count as 23 characters.
LinkedIn3,000Text, images, or one video.
Bluesky300Text, up to 4 images, or one video (not both).
TikTok2,200Requires a video or at least one image.
Instagram2,200Requires media: image, carousel, Reel or Story.
YouTube5,000Requires a video.
Facebook63,206Text, photos, or one video.
Threads500Text, or one image, or one video.
Pinterest500Requires 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.

post.completed
{
  "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.

Verifying a delivery (Node.js)
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.

claude_desktop_config.json
{
  "mcpServers": {
    "synapse": {
      "command": "npx",
      "args": ["-y", "synapse-social-mcp"],
      "env": {
        "SYNAPSE_API_KEY": "syn_your_key_here"
      }
    }
  }
}

Available tools

ToolWhat it does
list_accountsYour connected accounts and the exact platform names to use.
list_postsRecent posts, filterable by status.
get_postOne post, with the live URL or error for each account.
create_postDraft, schedule or publish now — with media, captions and options.
update_postEdit a draft, scheduled or failed post.
publish_postPublish now, or retry only the accounts that failed.
delete_postDelete a post, and the live copy where supported.
list_mediaFiles already in your media library.
upload_mediaUpload 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.

Something missing or unclear? Tell us on the Support page and we will fix it.