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.
Endpoints
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://…"
}Guides
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.
AI clients
MCP overview#
MCP is how an AI assistant gets hands. Connect Synapse to an MCP client and you can stop describing posts and just ask for them — “post this to Instagram and X” — in any client that speaks the protocol: Claude, Cursor and others.
There are two servers and they share the same tools. Pick by where you want it to run:
| Remote (by URL) | Local (npm package) | |
|---|---|---|
| Setup | Paste one URL, sign in, approve. | Edit a JSON config file by hand. |
| Needs an API key | No — you sign in instead. | Yes, pasted into the config. |
| Where it runs | On Synapse's servers. | On your own machine. |
| Works in | Desktop, web and mobile clients. | Desktop clients only. |
| Uploads local files | No — it cannot see your machine. | Yes, by file path. |
| Updates | Automatic. | Whenever npx fetches a new version. |
Not sure? Use the URL. It is one paste, it needs no API key, and it works on the web and on your phone as well as the desktop. Reach for the local package only when you want to publish files straight off your own machine.
Connect by URL#
Add this as a custom connector or remote MCP server in your client. There is nothing to install and no key to copy — your browser opens, you sign in to Synapse, you approve once, and the client is connected.
- Paste the URL where your client asks for a connector or remote MCP server.
- It sends you to Synapse. Sign in if you are not already, then read what the app is asking for and press Approve.
- That is it — ask your assistant what accounts you have connected.
Behind the scenes this is standard OAuth 2.1 with PKCE, so any spec-compliant client handles it without Synapse-specific setup. Access is granted to that one app, for your account only, and you can withdraw it at any time.
Some clients take a URL but cannot sign in. If yours has no browser step, the same URL also accepts an API key as a bearer token. Create one on the API Keys page and send it as a header.
Authorization: Bearer syn_your_keyLocal install#
The local server is an npm package that runs on your own computer. It is the right choice for one reason: because it runs where your files are, it can publish an image or video straight from a path on your disk.
Add this to your client’s MCP configuration — in Claude Desktop that is Settings → Developer → Edit Config — replacing the placeholder with a key from the API Keys page. Nothing to install first, since npx fetches it on demand. Quit and reopen the app fully afterwards, not just the window.
{
"mcpServers": {
"synapse": {
"command": "npx",
"args": ["-y", "synapse-social-mcp"],
"env": {
"SYNAPSE_API_KEY": "syn_your_key_here"
}
}
}
}The package is synapse-social-mcp on npm.
Tools#
Both servers expose the same tools, with one exception: upload_media takes a path on your computer, so only the local server can offer it. On the remote server, attach media that is already in your library instead — list_media gives you the paths.
| Tool | What it does | Remote |
|---|---|---|
| list_accounts | Your connected accounts and the exact platform names to use. | Yes |
| list_posts | Recent posts, filterable by status. | Yes |
| get_post | One post, with the live URL or error for each account. | Yes |
| create_post | Draft, schedule or publish now — with media, captions and options. | Yes |
| update_post | Edit a draft, scheduled or failed post. | Yes |
| publish_post | Publish now, or retry only the accounts that failed. | Yes |
| delete_post | Delete a post, and the live copy where supported. | Yes |
| list_media | Files already in your media library. | Yes |
| upload_media | Upload a local image or video by file path. | Local only |
TikTok publishes privately unless you say otherwise. Ask for a public TikTok post explicitly, or it goes out as SELF_ONLY— visible only to you. The tool descriptions tell your assistant this, so asking for “a public TikTok post” is enough.
Things to try
- Which social accounts do I have connected to Synapse?
- Schedule a LinkedIn post for Monday 9am Singapore time: “5 lessons from shipping in public”.
- Post the newest image in my Synapse media library to Instagram and X with the caption “Shipping day”.
- 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.
- Post this to Instagram and X: “Shipping day” with the image at ~/Desktop/launch.png(local server only)
Managing access#
Every app you connect by URL is listed on your API Keys page under Connected apps, with when you connected it and when it last did anything. Press Disconnect and it loses access on its very next request.
An app can only ever do what your own account can do, and it is scoped to your account alone. Access renews itself while the app is in use and lapses on its own if it goes unused for 30 days. Anything already published stays published — disconnecting stops future posts, it does not retract past ones.
Only approve a connection you started. Any developer can register an app and name it anything, so treat an unexpected approval screen the way you would an unexpected password prompt: if you did not just click connect in your own AI client, press Cancel.
Build a client#
Skip this unless you are writing an MCP client yourself — an off-the-shelf client discovers all of it automatically. The endpoint is https://withsynapse.app/api/mcp, spoken as JSON-RPC over Streamable HTTP. It is stateless: no session id is issued, so every POST stands alone.
An unauthenticated request answers 401 with a WWW-Authenticate header pointing at the protected-resource metadata, which is where discovery begins.
| Purpose | Endpoint | Spec |
|---|---|---|
| Protected-resource metadata | /.well-known/oauth-protected-resource | RFC 9728 |
| Authorization-server metadata | /.well-known/oauth-authorization-server | RFC 8414 |
| Register a client | POST /api/oauth/register | RFC 7591 |
| Authorize (consent) | GET /oauth/authorize | RFC 6749 |
| Exchange / refresh tokens | POST /api/oauth/token | RFC 6749 + 7636 |
Clients are public clients: register with token_endpoint_auth_method: "none" and prove yourself with PKCE instead. Only S256 is accepted. Redirect URIs are matched by exact string against what you registered, so register every callback you will use — https anywhere, plain http only on localhost or 127.0.0.1, and private app schemes such as cursor:// are fine.
Access tokens last an hour and refresh tokens rotate on every use, so store the new refresh token each time and discard the old one.