Socialync API
Publish, schedule and read posts from your own code. It is the same publishing engine as the Socialync MCP server, over plain JSON. n8n, Make, Zapier, GitHub Actions and your own scripts can all call it with one key.
Base URL: https://mcp.socialync.io/v1. The machine readable spec (OpenAPI 3.1) is at https://mcp.socialync.io/v1/openapi.json.
Authentication
Create a key on the API keys page. It is the same key the MCP server uses, and it can be limited to some of your profiles. Send it as a bearer token. OAuth tokens are not accepted here.
Authorization: Bearer sk_live_...Browsers cannot call the API: it sends no CORS headers. Keep keys on a server or in your CI secrets.
Quickstart
Check the key, your plan and your caps.
export SOCIALYNC_API_KEY=sk_live_...
curl https://mcp.socialync.io/v1/me \
-H "Authorization: Bearer $SOCIALYNC_API_KEY"Publish now. Text and up to 10 images go to Facebook, Instagram, LinkedIn, X, Threads and Bluesky.
curl -X POST https://mcp.socialync.io/v1/posts \
-H "Authorization: Bearer $SOCIALYNC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"platforms": ["linkedin", "bluesky"],
"caption": "Shipped from a script."
}'Schedule. With scheduledAt you can also post video and reach TikTok and YouTube.
# One hour from now (GNU date; on macOS use: date -u -v+1H +%Y-%m-%dT%H:%M:%SZ)
SCHEDULED_AT=$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)
curl -X POST https://mcp.socialync.io/v1/posts \
-H "Authorization: Bearer $SOCIALYNC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"platforms": ["instagram", "tiktok"],
"caption": "Friday drop",
"scheduledAt": "'"$SCHEDULED_AT"'",
"mediaId": "upload:abc123",
"tiktokPrivacyLevel": "PUBLIC_TO_EVERYONE"
}'Upload a file first when the media is not at a public URL.
# 1. Ask for an upload URL
curl -X POST https://mcp.socialync.io/v1/media \
-H "Authorization: Bearer $SOCIALYNC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"files":[{"fileName":"launch.mp4","contentType":"video/mp4"}]}'
# 2. PUT the file to the uploadUrl in the answer, with the same Content-Type
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @launch.mp4
# 3. Finalize with the id from step 1. The answer holds the mediaId.
curl -X POST https://mcp.socialync.io/v1/media/$UPLOAD_ID/finalize \
-H "Authorization: Bearer $SOCIALYNC_API_KEY"Quickstarts by language
The same three steps in each language: list your profiles, create a draft, schedule a post with an idempotency key. They are built from the OpenAPI document. A Postman collection of every endpoint is at https://mcp.socialync.io/v1/postman.json. Import it and paste your key into the apiKey variable.
curl
export SOCIALYNC_API_KEY=sk_live_...
# List your profiles
curl https://mcp.socialync.io/v1/profiles \
-H "Authorization: Bearer $SOCIALYNC_API_KEY"
# Create a draft (sends nothing)
curl -X POST https://mcp.socialync.io/v1/drafts \
-H "Authorization: Bearer $SOCIALYNC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"platforms":["linkedin","bluesky"],"caption":"A draft made from a script."}'
# Schedule a post, with an idempotency key
# scheduledAt is one hour from now (GNU date; on macOS use: date -u -v+1H +%Y-%m-%dT%H:%M:%SZ)
SCHEDULED_AT=$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)
curl -X POST https://mcp.socialync.io/v1/posts \
-H "Authorization: Bearer $SOCIALYNC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"platforms":["linkedin","bluesky"],"caption":"Scheduled from a script.","scheduledAt":"'"$SCHEDULED_AT"'"}'JavaScript (fetch)
// Node 20 or later, in an ES module (.mjs). Keep the key in an environment variable or a CI secret.
const BASE = 'https://mcp.socialync.io/v1';
const headers = {
Authorization: `Bearer ${process.env.SOCIALYNC_API_KEY}`,
'Content-Type': 'application/json',
};
async function api(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
return body;
}
// List your profiles
const profiles = await api('/profiles');
console.log(profiles.data);
// Create a draft (sends nothing)
const draft = await api('/drafts', {
method: 'POST',
body: JSON.stringify({
platforms: ['linkedin', 'bluesky'],
caption: 'A draft made from a script.',
}),
});
console.log(draft.id);
// Schedule a post, with an idempotency key
// scheduledAt is one hour from now. Send the same Idempotency-Key when you retry, so a retry cannot post twice.
const scheduled = await api('/posts', {
method: 'POST',
headers: { 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({
platforms: ['linkedin', 'bluesky'],
caption: 'Scheduled from a script.',
scheduledAt: new Date(Date.now() + 3600e3).toISOString(),
}),
});
console.log(scheduled.id, scheduled.status);Python (requests)
import os
import uuid
from datetime import datetime, timedelta, timezone
import requests
BASE = 'https://mcp.socialync.io/v1'
HEADERS = {'Authorization': f"Bearer {os.environ['SOCIALYNC_API_KEY']}"}
def api(method, path, **kwargs):
headers = {**HEADERS, **kwargs.pop('headers', {})}
res = requests.request(method, BASE + path, headers=headers, timeout=120, **kwargs)
body = res.json()
if not res.ok:
err = body['error']
raise RuntimeError(f"{res.status_code} {err['code']}: {err['message']}")
return body
# List your profiles
profiles = api('GET', '/profiles')
print(profiles['data'])
# Create a draft (sends nothing)
draft = api('POST', '/drafts', json={
'platforms': ['linkedin', 'bluesky'],
'caption': 'A draft made from a script.',
})
print(draft['id'])
# Schedule a post, with an idempotency key
# scheduledAt is one hour from now. Send the same Idempotency-Key when you retry, so a retry cannot post twice.
scheduled = api(
'POST',
'/posts',
headers={'Idempotency-Key': str(uuid.uuid4())},
json={
'platforms': ['linkedin', 'bluesky'],
'caption': 'Scheduled from a script.',
'scheduledAt': (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
},
)
print(scheduled['id'], scheduled['status'])How publishing works
- A call with your key is your instruction.
POST /v1/postspublishes without a confirmation step. If you switched on the publish confirmation in your Socialync settings, bothPOST /v1/postsand approving a draft answer 403direct_publish_disabled. Turn that setting off to publish through the API. Creating a draft still works. - To hold a post for a person to approve, use
POST /v1/draftsand thenPOST /v1/drafts/{id}/approve. A draft nobody approves expires after 24 hours. - Content written or made by an AI model must be approved by a person before it is scheduled or published. Send it as a draft and let the account owner approve it. Section 10 of the Developer Agreement covers this, and the agreement applies to every call.
- An immediate post is text and images. Video, TikTok and YouTube need
scheduledAt, a few minutes ahead or later. imageUrlsandvideoUrlare copied into Socialync storage when you schedule, so a link only has to work at that moment.- The monthly allowance of your plan, the daily post cap per profile, the platforms' own limits and the X and YouTube rules are the same as in the app and the MCP server. Posts to several platforms count once against the daily cap.
- When some platforms publish and some fail, the status is 207 and
resultssays which. - Add
?view=historytoGET /v1/postsfor finished multi platform posts. Scheduled posts keep the id the create call returned.
Limits and retries
- 60 requests per minute per key. Past that you get 429 with
Retry-After. Every response carriesRateLimit-Limit,RateLimit-RemainingandRateLimit-Reset. - Each key can have 5 requests running at once. The service also caps the total across all keys. Past either cap you get 429 with
Retry-After. Wait for a running request to finish before you send the next. - Request bodies are limited to 256 KB. Images do not go in the body. Upload them with
POST /v1/mediaor pass a URL. - Send an
Idempotency-Keyheader on every POST you might retry. The same key with the same body returns the first response for 24 hours, so a retry cannot post twice. The same key with a different body is a 422. - A request that is refused (a 4xx) frees its key, so you can fix the body and send it again with the same key. A request that ends in a 500 or 504 may still have gone out, so the key keeps returning that answer. Check
GET /v1/posts, then use a new key. If the API could not record the result of a request, the key answers 409idempotency_outcome_unknownuntil it expires and does not run the request again. - Lists return 20 items by default, at most 100. Pass the
nextCursorfrom a response ascursorto get the next page.
Errors
Every error has the same shape. Switch on error.code. The message is for people and can change.
{
"error": {
"code": "daily_cap_reached",
"message": "Daily draft cap reached: 10 drafts per UTC day per profile on this plan.",
"docs": "https://www.socialync.io/developers/api#daily_cap_reached"
}
}| Code | Status | When | What to do |
|---|---|---|---|
| invalid_request | 400 | The body or query string is not valid, or a field the API does not know was sent. | Read error.details: each entry names the field and the problem. Fix the request and send it again. |
| invalid_json | 400 | The request body is not valid JSON. | Send a JSON object. Check for a trailing comma or an unescaped quote. |
| unauthorized | 401 | The key is missing, malformed, revoked, or the account is not active. | Send Authorization: Bearer sk_live_... with a current key from the API keys page. OAuth tokens are not accepted. |
| plan_not_allowed | 403 | The plan does not include what you asked for. | error.message names the feature. Use an allowed option, or change plan in Socialync. |
| profile_not_authorized | 403 | The profile you named is not one this key can use. | Call GET /v1/profiles to see the profiles this key can use, and send one of those ids. |
| direct_publish_disabled | 403 | Your Socialync settings ask for confirmation before posts go out, and the API cannot show that dialog. POST /v1/posts and POST /v1/drafts/{id}/approve are both refused. | Turn the publish confirmation off in your settings to publish through the API. POST /v1/drafts still works. |
| not_found | 404 | No such resource on a profile this key can use, or no such endpoint. | Check the id. Scheduled posts and the last 100 history entries can be read by id; drafts are at /v1/drafts/{id}. Endpoints are listed in /v1/openapi.json. |
| method_not_allowed | 405 | The path exists but not for this method. | The Allow header lists the methods the path accepts. |
| conflict | 409 | The request clashes with the current state, for example media that is not finalized yet or a post that already went out. | error.message says what to do first. Where it applies, error.details has one entry per item. |
| request_in_flight | 409 | An identical request, or one with the same Idempotency-Key, is still running. | Wait for it instead of retrying. Retry-After says how long. |
| idempotency_outcome_unknown | 409 | A request with this Idempotency-Key started and its result was not recorded, so it may or may not have gone through. It is not run again with that key. | Check GET /v1/posts, then use a new Idempotency-Key. |
| unsupported_media_type | 415 | The body was not sent as JSON. | Send Content-Type: application/json. |
| payload_too_large | 413 | The request body is over the size limit. | Images do not go in the body. Upload them with POST /v1/media, or pass imageUrls. |
| validation_failed | 422 | The request is well formed but the post cannot be made as asked. | error.message says what to change, and error.details lists each item when there are several. |
| unsupported_for_immediate | 422 | An immediate post was asked for something only a scheduled post can do: video, TikTok, YouTube, a title, or the TikTok privacy level or the Instagram trial reel option. | Set scheduledAt a few minutes ahead, or drop the field or platform named in error.message. |
| idempotency_key_reuse | 422 | That Idempotency-Key was already used with a different request. | Use a new key for a new request. Reuse a key only to retry the same request. |
| rate_limited | 429 | Too many requests per minute for this key, too many requests running at once, or too many failed attempts from one address. | Wait the number of seconds in Retry-After. RateLimit-Remaining on authenticated responses shows how many are left. |
| daily_cap_reached | 429 | The daily post or draft cap for that profile is used up. It resets at midnight UTC. | Retry after the time in Retry-After, or post to a different profile. |
| plan_limit_reached | 429 | The monthly post allowance of the plan is used up. | Wait for the allowance to renew, or change plan in Socialync. |
| quota_cooldown | 429 | A platform limit applies for now, for example a YouTube quota window. | Retry after the time in Retry-After. |
| internal_error | 500, 502, 503 | Something failed on our side, or an answer could not be read. The request may not have completed. A 503 means authentication or the key lookup is briefly unavailable. | Check GET /v1/posts before you retry a post. For a 503, retry after the seconds in Retry-After. |
| publish_failed | 502 | No platform accepted the post. | error.details.results has the reason for each platform. Fix that, then post again. |
| idempotency_unavailable | 503 | The Idempotency-Key could not be checked, so the request did not run. | Retry after the number of seconds in Retry-After. |
| upstream_timeout | 504 | The request took too long and may still have completed. | Check GET /v1/posts before you retry, so nothing is created twice. |
Endpoints
| Method | Path | What it does |
|---|---|---|
| GET | /v1/me | Plan, quota and caps |
| GET | /v1/profiles | Profiles this key can use |
| GET | /v1/connections | Connected platform accounts for a profile |
| POST | /v1/media | Get presigned upload URLs |
| POST | /v1/media/{id}/finalize | Finish an upload and get its mediaId |
| POST | /v1/posts/validate | Check a post without creating anything |
| POST | /v1/posts | Publish now, or schedule |
| GET | /v1/posts | Scheduled posts and history |
| GET | /v1/posts/{id} | One post |
| PATCH | /v1/posts/{id} | Change a scheduled post |
| DELETE | /v1/posts/{id} | Cancel a scheduled post |
| POST | /v1/drafts | Hold a post for approval |
| GET | /v1/drafts/{id} | A draft and its state |
| POST | /v1/drafts/{id}/approve | Publish or schedule a draft |
| GET | /v1/analytics | Analytics for a profile |
Request and response schemas for every endpoint are in the OpenAPI document.
