BackSocialCannon API Docs
Get Started

API Reference

SocialCannon provides a REST API for publishing to 6 social platforms (Twitter, Facebook, Instagram, LinkedIn, TikTok, YouTube), analytics, engagement monitoring, A/B testing, AI-powered content repurposing, and 23 MCP tools for AI agents. OpenClaw ready.

Quick Start

1. Get an API token
curl -X POST https://socialcannon.app/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_SECRET" }'
2. Create a post
curl -X POST https://socialcannon.app/api/v1/posts \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "content": "Hello from SocialCannon!"
  }'
3. Post with an image

The mediaUrls field accepts any public URL — you can use an image already hosted elsewhere, or upload via the media endpoint first.

Option A: Use an external URL directly

curl -X POST https://socialcannon.app/api/v1/posts \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "content": "Check out this photo!",
    "mediaUrls": ["https://example.com/photo.jpg"]
  }'

Option B: Upload via signed URL (direct-to-GCS)

# Step 1: Ask for a signed PUT URL
curl -X POST https://socialcannon.app/api/v1/media/upload-init \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "photo.jpg", "contentType": "image/jpeg", "size": 123456 }'
# Returns: { "success": true, "data": { "uploadUrl": "...", "publicUrl": "...", "requiredHeaders": { ... } } }

# Step 2: PUT the bytes directly to GCS (file size up to 4GB)
curl -X PUT "<uploadUrl>" \
  -H "Content-Type: image/jpeg" \
  -H "x-goog-acl: public-read" \
  -H "x-goog-content-length-range: 0,4294967296" \
  -H "x-goog-if-generation-match: 0" \
  -H "x-goog-meta-socialcannon-client-key: <copy exact value from requiredHeaders>" \
  -H "x-goog-meta-socialcannon-upload-state: quota-reserved" \
  --data-binary "@photo.jpg"

# Step 3: Finalize so usage counter increments and lifecycle clock starts
curl -X POST https://socialcannon.app/api/v1/media/upload-complete \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "publicUrl": "<publicUrl from step 1>" }'

# Step 4: Use the publicUrl when creating the post
curl -X POST https://socialcannon.app/api/v1/posts \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "content": "Check out this photo!",
    "mediaUrls": ["<publicUrl>"]
  }'

Bytes go straight to Google Cloud Storage — SocialCannon never touches them, so files up to 4GB upload cleanly. Up to 4 images for Twitter, 10 for Instagram carousels.

4. Schedule for later
curl -X POST https://socialcannon.app/api/v1/posts \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "content": "Scheduled post!",
    "scheduledAt": "2026-04-01T10:00:00Z"
  }'

Authentication

SocialCannon uses OAuth2 client_credentials flow. Exchange yourclient_id andclient_secret for a JWT token that expires in 1 hour.

Include the token in all authenticated requests:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Available scopes:

posts:readposts:writeposts:deleteaccounts:readaccounts:writemedia:uploadanalytics:readlinks:readlinks:writeengagements:readengagements:write

Rate limiting:

Free: 30 requests/minute. Pro: 300 requests/minute. Returns 429 with Retry-After header when exceeded.

Subscription Tiers

Free

2 connected accounts

10 posts/period, 2 scheduled

1 Twitter/X post per period (hard cap)

All 6 platforms

No analytics, no replies

2-item threads, 5 tracked links

3 media uploads/period

30 API requests/min

Pro — $15/mo

Unlimited accounts & posts

50 Twitter/X posts per period

Full analytics with history

25-item threads, unlimited tracked links

Engagement replies, A/B testing (5×4)

Timing suggestions, 90-day calendar

300 API requests/min

Agency — $49/mo

Everything in Pro

250 Twitter/X posts per period

A/B testing (20 concurrent, 6 variants)

365-day calendar

Priority support

600 API requests/min

Enterprise — custom

Custom Twitter/X quota

Custom rate limits + SLA

Dedicated support

Custom integrations

1200 API requests/min default

Contact sales

Response Format

Success
{
  "success": true,
  "data": { ... }
}
Error
{
  "success": false,
  "error": "Description of the error",
  "code": "LIMIT_EXCEEDED",
  "details": { ... }
}

REST API Endpoints

Quick Start

INFO
Getting started — publish your first postPublic

1. Get your credentials from the dashboard Settings page (a client is auto-provisioned on signup): the client_id is always shown; the client_secret is revealed once — click "Generate API Secret" to issue a new one, which revokes the old secret 2. Get a token: POST /api/v1/auth/token with your credentials 3. Connect a social account: POST /api/v1/accounts/connect-token with the platform, then open the returned connectUrl in a browser (dashboard users just click Connect — their session authorizes the link) 4. List connected accounts: GET /api/v1/accounts (use the account id from the response) 5. Publish a post: POST /api/v1/posts with accountId, content, and optional mediaUrls

// Example: publish a text post to a connected account
POST /api/v1/posts
Authorization: Bearer YOUR_TOKEN

{
  "accountId": "your-account-id",
  "content": "Hello from SocialCannon!"
}

// Response:
{
  "success": true,
  "data": {
    "id": "post-id",
    "status": "published",
    "platformPostId": "...",
    "platformPostUrl": "https://..."
  }
}
INFO
Platform capabilitiesPublic

Each platform supports different content types. Use the mediaType field in platformOptions to control how content is published.

Facebook:
  • Feed post (text, images, or video)
  • Story (single image or video, mediaType: "story")
  • Reel (single video, mediaType: "reel")

Instagram:
  • Feed post (images required, up to 10 for carousel)
  • Story (single image or video, mediaType: "story")
  • Reel (single video, mediaType: "reel")

Twitter/X:
  • Tweet (text, optional images or video)

TikTok:
  • Video post (single video required)
  • Photo carousel (multiple images, up to 35)
  • Story (single image or video, mediaType: "story")
  • privacyLevel required on all posts (use get_tiktok_creator_info to get allowed values)

YouTube:
  • Video upload (single video, mediaType: "feed")
  • Short (single vertical video ≤60s, mediaType: "short")
  • Community post (text only, mediaType: "community")

Authentication

POST
/api/v1/auth/tokenPublic

Exchange client credentials for a JWT access token (OAuth2 client_credentials flow).

{ "grant_type": "client_credentials", "client_id": "...", "client_secret": "sc_..." }
Response:
{ "success": true, "data": { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "scope": "posts:read posts:write ..." } }
GET
/api/v1/auth/identity

Attest the active bearer token's own client ID and creation-time client name. Returns no secret, scopes, trust flag, or tenant metadata.

Scope: None beyond a valid active client token
Response:
{ "success": true, "data": { "identityContractVersion": 1, "client_id": "...", "name": "My Client" } }
GET
/api/v1/auth/clients

List all API clients.

Scope: accounts:read
POST
/api/v1/auth/clients

Create a new API client. Returns the secret once.

Scope: clients:manage
{ "name": "My Client", "scopes": ["posts:read", "posts:write"] }

Posts

GET
/api/v1/posts

List posts. Supports cursor-based pagination.

Scope: posts:read
Query params: status, platform, accountId, limit, cursor
POST
/api/v1/posts

Create a post. Omit scheduledAt to publish immediately. Media URLs must be publicly accessible. Maximum scheduling window: 30 days. An optional Idempotency-Key header (8-128 printable ASCII characters, no spaces) binds one canonical request to one durable post ID; exact replays never publish again and set Idempotency-Replayed: true. A different request under the same key returns 409 IDEMPOTENCY_CONFLICT. In-progress or provider-uncertain replays return 409 with the stable data.id and must be recovered, never compensated as a new post. Set platformOptions.aiGenerated: true to request the platform's native AI-content label. TikTok and YouTube support it; Instagram requests fail validation unless the deployment has explicitly enabled verified native-disclosure access — see the platformOptions reference.

Scope: posts:write
{
  "accountId": "abc123",
  "content": "Hello world!",
  "mediaUrls": ["https://example.com/video.mp4"],
  "scheduledAt": "2026-04-01T10:00:00Z",
  "platformOptions": {
    "mediaType": "reel",
    "aiGenerated": true
  }
}
Response:
// Success:
{ "success": true, "data": { "id": "...", "status": "published", "platformPostId": "...", "platformPostUrl": "..." } }

// Provider acknowledged, local persistence failed (HTTP 502):
{ "success": false, "error": "...", "data": { "id": "...", "platformPostId": "...", "platformPostUrl": "...", "providerPublished": true, "reconciliationRequired": true } }

// Exact keyed replay still in progress (HTTP 409; never delete/redispatch this ID):
{ "success": false, "error": "...", "code": "IDEMPOTENCY_IN_PROGRESS", "data": { "id": "...", "status": "draft" } }
GET
/api/v1/posts/idempotency

Recover the public state of a keyed POST /api/v1/posts operation. Send the original Idempotency-Key in the request header, not the URL. A 404 means no durable binding exists at that read; another same-key request can still be in preflight.

Scope: posts:read
Response:
// Found:
{ "success": true, "data": { "id": "...", "status": "failed", "platformPostId": "...", "platformPostUrl": "...", "recovery": { "required": true, "code": "PROVIDER_RECONCILIATION_REQUIRED", "kind": "publish_outcome" } } }

// Not found (HTTP 404):
{ "success": false, "error": "No post was found for this Idempotency-Key", "code": "IDEMPOTENCY_NOT_FOUND" }
GET
/api/v1/posts/:id

Get a single post by ID.

Scope: posts:read
PATCH
/api/v1/posts/:id

Update a draft or scheduled post.

Scope: posts:write
{ "content": "Updated text", "scheduledAt": "..." }
DELETE
/api/v1/posts/:id

Delete a post. Every delete now fails closed when the record contains provider-publish evidence: SocialCannon must verify any required provider deletion before removing the local record. For rollback/reconciliation, send { strictCompensation: true, platformPostId? }; the optional provider ID must exactly match persisted evidence. Unsupported or failed provider deletion returns a non-2xx response and leaves the local record available for operator reconciliation. A post with an uncertain publish attempt and no provider ID cannot be deleted until it is reconciled.

Scope: posts:delete
{ "strictCompensation": true, "platformPostId?": "exact provider post id" }

Threads

POST
/api/v1/posts/thread

Create a multi-part thread (Twitter thread or Instagram carousel). Publishes immediately or schedules for later. Maximum scheduling window: 30 days. Accepts platformOptions, including aiGenerated: true. Instagram rejects that request before publishing unless verified native-disclosure access is enabled for the deployment.

Scope: posts:write
{
  "accountId": "abc123",
  "items": [
    { "content": "Thread part 1..." },
    { "content": "Thread part 2...", "mediaUrls": ["https://..."] }
  ],
  "scheduledAt": "2026-04-01T10:00:00Z"
}

Accounts

GET
/api/v1/accounts

List all connected social media accounts.

Scope: accounts:read
GET
/api/v1/accounts/:id

Get a single account. Tokens are stripped from the response.

Scope: accounts:read
DELETE
/api/v1/accounts/:id

Disconnect a social account.

Scope: accounts:write
GET
/api/v1/accounts/:id/tiktok/creator-info

TikTok only. Returns the account's live posting capabilities for building a compliant post UI: allowed privacy levels (`privacyLevelOptions`), whether Comment/Duet/Stitch are disabled, the creator nickname/username, and `maxVideoPostDurationSec`.

Scope: accounts:read
Response:
{ "success": true, "data": { "creatorNickname": "...", "creatorUsername": "...", "creatorAvatarUrl": "...", "privacyLevelOptions": ["PUBLIC_TO_EVERYONE","SELF_ONLY"], "commentDisabled": false, "duetDisabled": false, "stitchDisabled": false, "maxVideoPostDurationSec": 600 } }

Media

POST
/api/v1/media/upload-init

Step 1 — initialize a direct-to-GCS upload. Returns a 15-minute V4 signed PUT URL plus the final public URL. Enforces subscription quota before issuing the URL.

Scope: media:upload
{ "filename": "clip.mp4", "contentType": "video/mp4", "size": 12345678 }
Response:
{ "data": { "uploadUrl": "https://storage.googleapis.com/...", "publicUrl": "https://storage.googleapis.com/...", "requiredHeaders": { "Content-Type": "video/mp4", "x-goog-acl": "public-read", "x-goog-content-length-range": "0,4294967296", "x-goog-if-generation-match": "0", "x-goog-meta-socialcannon-client-key": "...", "x-goog-meta-socialcannon-upload-state": "quota-reserved" } } }
PUT
{uploadUrl}Public

Step 2 — upload the file bytes directly to GCS using the signed URL from step 1. Must include the headers from `requiredHeaders` exactly or the signature check fails. Supports files up to 4GB.

Raw binary file contents
Response:
200 OK (no body)
POST
/api/v1/media/upload-complete

Step 3 — finalize the upload: stamps the lifecycle retention timer (customTime) and increments your `mediaUploadsThisPeriod` counter. Verifies the object exists in your client prefix before bookkeeping.

Scope: media:upload
{ "publicUrl": "https://storage.googleapis.com/.../media/{clientId}/{uuid}.{ext}" }
Response:
{ "data": { "url": "...", "filename": "...", "contentType": "...", "size": 102400 } }

Calendar

GET
/api/v1/calendar

Get a content calendar view with posts, gap analysis, and summary stats for a date range (max 90 days).

Scope: posts:read
Query params: startDate (required), endDate (required), accountId?, platform?
Response:
{ "posts": [...], "summary": { "totalPosts", "postsByPlatform", "postsByDay", "gaps": [{ "date", "dayOfWeek" }] } }
GET
/api/v1/calendar/slots

Find available time slots not occupied by scheduled or published posts (max 14-day range).

Scope: posts:read
Query params: startDate (required), endDate (required), slotDurationMinutes? (30-1440, default: 60)
Response:
{ "slots": [{ "start", "end", "available" }], "totalSlots", "availableSlots", "occupiedSlots" }

Analytics

GET
/api/v1/posts/:id/analytics

Fetch live engagement metrics from the platform, store a snapshot, and return current + history.

Scope: posts:read
Response:
{ "current": { "metrics": { "likes", "comments", "shares", "impressions", ... }, "fetchedAt" }, "history": [...] }
GET
/api/v1/analytics/summary

Get aggregate analytics across all posts for a date range.

Scope: posts:read
Query params: startDate (required), endDate (required), platform?, accountId?
Response:
{ "totalPosts", "totalLikes", "totalComments", "totalShares", "totalImpressions", "avgEngagementRate" }
POST
/api/v1/analytics/refresh

Bulk-refresh analytics for multiple posts (max 50 per request).

Scope: posts:read
{ "postIds?": ["..."], "platform?": "twitter", "limit?": 20 }
Response:
{ "refreshed": 15, "failed": 2, "results": [{ "postId", "success", "error?" }] }

Links & UTM

GET
/api/v1/links

List tracked links with UTM parameters. Supports cursor-based pagination.

Scope: links:read
Query params: postId?, platform?, limit?, cursor?
POST
/api/v1/links/generate

Generate a UTM-tagged URL. Optionally save it for tracking.

Scope: links:write
{ "url": "https://example.com", "platform?": "twitter", "campaign?": "launch", "content?": "cta-btn", "postId?": "...", "save?": true }
Response:
{ "originalUrl": "https://example.com", "trackedUrl": "https://example.com?utm_source=twitter&...", "linkId?": "..." }

Engagements

GET
/api/v1/posts/:id/engagements

Fetch comments and replies from the platform for a published post. Stores new engagements.

Scope: engagements:read
Query params: cursor?
Response:
{ "engagements": [{ "type", "authorName", "content", "createdAt", ... }], "nextCursor?" }
GET
/api/v1/engagements

List all stored engagements across posts. Filter by read status for an inbox view.

Scope: engagements:read
Query params: postId?, isRead? (true/false), limit?, cursor?
PATCH
/api/v1/engagements/:id

Mark an engagement as read.

Scope: engagements:write
POST
/api/v1/engagements/:id/reply

Reply to a comment or mention directly on the social platform.

Scope: engagements:write
{ "content": "Thanks for the feedback!" }
Response:
{ "platformReplyId": "...", "engagementId": "...", "repliedAt": "..." }

Content Repurposing

POST
/api/v1/posts/repurpose

Use AI to adapt content for multiple platforms. Two modes: "preview" returns adapted variants for review, "post" adapts and publishes in one call. Content is humanized to remove AI writing patterns. Trusted clients bypass tier limits.

Scope: posts:write
{
  "sourceContent": "Your original text...",
  "targetPlatforms": ["twitter", "facebook", "tiktok"],
  "mode?": "preview",
  "tone?": "professional",
  "accountIds?": { "twitter": "acc_1", "facebook": "acc_2" },
  "mediaUrls?": { "twitter": ["https://..."] },
  "appendContent?": { "twitter": "Extra text for Twitter" },
  "appendToAll?": "Appended to all platforms"
}
Response:
preview: { "variants": [{ "platform", "content", "validation", "characterCount" }], "allValid" }
post: { "results": [{ "platform", "success": true, "postId", "postUrl?", "platformPostId?" } | { "platform", "success": false, "postId?", "platformPostId?", "providerPublished?", "reconciliationRequired?", "error" }] }
Every created post-mode result includes its SocialCannon postId, including failed sandbox dry runs. If providerPublished is true, retain platformPostId and use strict compensation or operator reconciliation.

A/B Testing

Pro
POST
/api/v1/ab-tests

Create an A/B test. Requires an Idempotency-Key header (8-128 printable ASCII characters, no spaces); reuse the exact key after a timeout or lost response. The test row, deterministic variant post rows, usage reservations, and active-test slot are provisioned atomically before scheduler or provider visibility. If scheduledAt is omitted, the pre-provisioned variants publish immediately through provider mutation fences; if scheduledAt is provided, all variants are scheduled atomically for that time (within 30 days). A new operation returns 201; an exact committed replay returns 200 with replayed: true; the same key with a different payload returns 409 IDEMPOTENCY_CONFLICT. Immediate uncertain outcomes retain the test in provisioning with exact post IDs and return 502 for reconciliation; an operator-confirmed absent outcome is consumed once on same-key replay before untouched variants resume. Definitive failed variants are excluded from comparison. Once all variants settle, at least two published variants activate the test; otherwise it becomes failed with terminalReason insufficient_eligible_variants and transactionally releases its active slot. Scheduled tests follow the same terminal rule. Test duration starts at the latest durable eligible-variant publish, and completion waits for analytics on every eligible variant. Per-variant mediaUrls optional. Not supported for TikTok accounts (A/B variants cannot set the privacy level TikTok requires) or platforms whose post analytics are unavailable. Set aiGenerated: true to request the native label on every variant. Instagram rejects that request before publishing unless verified native-disclosure access is enabled for the deployment.

Scope: posts:write
Header: Idempotency-Key: ab-test-launch-2026-04-20

{
  "accountId": "abc123",
  "name": "CTA test",
  "variants": [
    { "content": "Check out our new feature!", "mediaUrls": ["https://..."] },
    { "content": "You won't believe this new feature..." }
  ],
  "metric?": "engagementRate | likes | impressions | clicks",
  "minDurationHours?": 24,
  "scheduledAt?": "2026-04-20T10:00:00Z",
  "aiGenerated?": true
}
Response:
201 new / 200 replay: { "success": true, "replayed?": true, "data": { "id": "...", "status": "provisioning | active | failed", "startedAt?": "...", "terminalReason?": "insufficient_eligible_variants", "variants": [{ "id", "postId", "content", "state", "platformPostId?" }], ... } }
409: { "success": false, "code": "IDEMPOTENCY_CONFLICT | SCHEDULE_EXPIRED", "error": "..." }
502 partial/reconciliation: { "success": false, "code?": "PARTIAL_FAILURE | PROVIDER_RECONCILIATION_REQUIRED | INSUFFICIENT_ELIGIBLE_VARIANTS", "error": "...", "data": { "id", "status": "provisioning | active | failed", "variants": [{ "id", "postId", "state", "platformPostId?" }] } }
GET
/api/v1/ab-tests

List A/B tests. Filter by status.

Scope: posts:read
Query params: status? (provisioning/active/completed/canceled/failed), limit?, cursor?
GET
/api/v1/ab-tests/:id

Get test details with live per-variant analytics and current winner.

Scope: posts:read
Response:
{ "variants", "variantResults": [{ "variantId", "metrics" }], "currentWinner", "confidence" }
POST
/api/v1/ab-tests/:id/complete

Force-complete an active test only after at least two eligible variants are durably published and each has analytics; atomically releases its active-test slot.

Scope: posts:write
DELETE
/api/v1/ab-tests/:id

Cancel an active or provisioning test and atomically release its active-test slot. Variant posts remain as regular posts.

Scope: posts:write

Timing Suggestions

Pro
GET
/api/v1/accounts/:id/timing

Analyze posting history and engagement to recommend optimal days and hours for posting.

Scope: analytics:read
Query params: timezone? (UTC, UTC+5, UTC-8 — default: UTC), platform?
Response:
{ "suggestions": [{ "day", "hour", "engagementRate", "sampleSize" }], "timezone" }

Platforms

GET
/api/v1/platformsPublic

List all supported platforms and their capabilities. Public endpoint, no auth required.

INFO
sandbox platformPublic

The sandbox platform is a dry-run test platform — nothing is published anywhere. Create a post with accountId: "sandbox" and a sandbox account is auto-provisioned on first use (no OAuth, no tokens). Publish returns a simulated success (fake sandbox_* post ID and URL). Sandbox posts do not consume your monthly post quota and work on every plan, so you can try the full API — create, publish, schedule, MCP tools — before connecting a real account. Sandbox usage is bounded by a lifetime allowance of 100 dry runs per account; media attachments are accepted but discarded, and auto-UTM is skipped.

Platform Options

INFO
platformOptions referencePublic

The platformOptions field in POST /api/v1/posts accepts platform-specific options. Use mediaType to control how content is published.

mediaType values:
• "reel" — Facebook or Instagram Reel (single video, MP4/MOV)
• "story" — Facebook, Instagram, or TikTok Story (single media, 24h ephemeral)
• "short" — YouTube Short (single video, vertical ≤60s)
• "community" — YouTube Community tab post (text only)

Media rules:
• Stories, Reels, and Shorts accept only one media attachment
• Facebook/Instagram feed posts accept multiple images (carousel, max 10)
• TikTok photo carousel accepts up to 35 images
• TikTok video posts accept only one video
• YouTube videos/shorts accept only one video file
• All media URLs must be publicly accessible

YouTube-specific options:
• "title" — Video title (max 100 chars)
• "tags" — Comma-separated video tags
• "categoryId" — YouTube category ID (default: "10" Music)
• "privacyStatus" — "public", "unlisted", or "private"

TikTok-specific options (call GET /api/v1/accounts/:id/tiktok/creator-info first):
• "privacyLevel" — REQUIRED. One of: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Choose from privacyLevelOptions returned by creator-info. Omitting this field returns 400 TIKTOK_PRIVACY_REQUIRED.
• "disableComment" — boolean (default: false). Set true to disable comments. Must be true if commentDisabled is true on creator-info.
• "disableDuet" — boolean (default: false). Video only. Set true to disable duets. Must be true if duetDisabled is true on creator-info.
• "disableStitch" — boolean (default: false). Video only. Set true to disable stitches. Must be true if stitchDisabled is true on creator-info.
• "commercialContent" — boolean. Set true if the post promotes a brand, product, or service (enables commercial content disclosure).
• "brandOrganic" — boolean. Set true if promoting your own brand ("Your Brand" disclosure). Requires commercialContent: true.
• "brandedContent" — boolean. Set true for paid/third-party partnerships. Requires commercialContent: true. Cannot be combined with privacyLevel: SELF_ONLY.

AI-generated content disclosure (cross-platform):
• "aiGenerated" — boolean (default: false). Set true when the post's media is AI-generated. SocialCannon maps it to the platform's native AI-content label:
  - Instagram: fails validation by default because native disclosure is not yet enabled for this deployment. When verified access is explicitly enabled, sends is_ai_generated on the media container (carousel parent only)
  - TikTok: is_aigc ("AI-generated" label)
  - YouTube: containsSyntheticMedia ("Altered content" disclosure)
  Ignored on platforms without a disclosure field (Facebook, X, LinkedIn).
  Also accepted by POST /api/v1/posts/thread (platformOptions), per-post in POST /api/v1/posts/auto-schedule (posts[].platformOptions), and as a test-level "aiGenerated" flag on POST /api/v1/ab-tests.

OAuth Connect

POST
/api/v1/accounts/connect-token

Mint a short-lived (10 min) connect token that authorizes one OAuth connect flow for your client. Required scope: accounts:write. Returns a ready-to-open connectUrl — send the user there in a browser.

{ "platform": "twitter", "redirectTo": "/dashboard/accounts" }
Response:
{ "data": { "connectUrl": "https://…/api/connect/twitter?connect_token=…", "expiresIn": 600 } }
GET
/api/connect/:platformPublic

Initiate OAuth connection for a social platform. Redirects to the platform's authorization page. Supported platforms: twitter, facebook, instagram, linkedin, tiktok, youtube. Caller must prove tenancy: either a signed-in dashboard session (the account is linked to the session's client) or a connect_token minted via POST /api/v1/accounts/connect-token. A bare client_id is not accepted.

Query params: connect_token (required unless signed in), redirect_to (optional callback URL)
GET
/api/connect/:platform/callbackPublic

OAuth callback handler. Exchanges authorization code for tokens and stores the connected account. Do not call directly — the platform redirects here after authorization.

Available Scopes

INFO
Scope referencePublic

Scopes control what an API client can access. Assign scopes when creating a client via POST /api/v1/auth/clients.

posts:read — List and view posts
posts:write — Create, update, schedule posts
posts:delete — Delete posts
accounts:read — List connected accounts
accounts:write — Connect/disconnect social accounts
media:upload — Upload media files
analytics:read — View analytics and timing suggestions
engagements:read — View comments and replies
engagements:write — Reply to engagements, mark as read
links:read — View tracked links
links:write — Generate UTM-tagged links
clients:manage — Create and list API clients

Connecting Social Accounts

Social accounts are connected via browser-based OAuth flows — this step requires a human to log in to the platform, so it cannot be completed by an unattended agent. Each connected account also carries a tokenHealth field; skip or reconnect any account whose token is expired before publishing. Redirect users to the connect endpoint for the desired platform:

PlatformConnect URLNotes
Twitter / X/api/connect/twitter?connect_token=...OAuth2 with PKCE
Facebook/api/connect/facebook?connect_token=...Page-level access
Instagram/api/connect/instagram?connect_token=...Requires media (no text-only)
LinkedIn/api/connect/linkedin?connect_token=...Post-only (no analytics/engagement)

After authorization, the user is redirected back to your app with the account connected. Tokens are encrypted with AES-256-GCM before storage.

Post Lifecycle

draftscheduledpublishingpublished
&searr;failed(after 3 retries with 5min backoff)
  • No scheduledAt → immediate publish
  • With scheduledAt → the requested time is preserved without rounding (up to 30 days ahead)
  • The per-minute scheduler picks up due posts at or after that time. Queue load and provider processing can delay delivery; exact-second delivery is not guaranteed.
  • Deleting a published post also attempts platform-side deletion
  • Thread posts follow the same lifecycle (each item published sequentially)

Platform-Specific Notes

Twitter / X

280 character limit

Up to 4 images per tweet

Threads via reply chains

v2 API for tweets, v1.1 for media upload

Facebook

63,206 character limit

Page-level access tokens

Supports native scheduling via API

Analytics and engagement pending Meta review

Instagram

2,200 character limit

Requires media (no text-only posts)

Max 10 carousel items, no API deletion

Threads use carousel container model

LinkedIn

3,000 character limit

Text-only or single/multi-image posts

Threads combine items into one post

Post-only (no analytics or engagement)

MCP Server (for AI Agents)

SocialCannon includes an MCP server with 23 tools that expose all functionality for AI agents (Claude, GPT, etc.) via the @modelcontextprotocol/sdk over stdio transport.

Start the MCP server (repo):

npm run mcp

Or run the published package directly (external clients):

npx -y @socialcannon/mcp

Set SOCIALCANNON_CLIENT_ID and SOCIALCANNON_CLIENT_SECRET (from your dashboard) before running.

Configuration (preferred — auto-refreshes JWT):

MCP_CLIENT_ID=<your API client ID>
MCP_CLIENT_SECRET=<your API client secret>
SOCIALCANNON_API_URL=https://socialcannon.app

Alternatively, set MCP_API_TOKEN with a pre-generated JWT (expires after 1 hour, no auto-refresh).

Available Tools (23)

Core

list_platforms

List all supported social media platforms and their capabilities.

Params: None

list_accounts

List all connected social media accounts.

Params: platform?

get_tiktok_creator_info

Fetch a TikTok account's posting capabilities — allowed privacy levels, disabled interactions, nickname, max video duration. Call before create_post for TikTok.

Params: accountId

create_post

Create and publish or schedule a social media post. For TikTok, privacyLevel is required (from get_tiktok_creator_info); also supports disableComment, disableDuet (video), disableStitch (video), commercialContent, brandOrganic, brandedContent. Set aiGenerated to request the native label on TikTok or YouTube. Instagram rejects that request unless verified native-disclosure access is enabled for the deployment.

Params: accountId, content, mediaUrls?, scheduledAt?, visibility?, privacyLevel?, disableComment?, disableDuet?, disableStitch?, commercialContent?, brandOrganic?, brandedContent?, aiGenerated?

list_posts

List social media posts with optional filters.

Params: status?, platform?, accountId?, limit?

get_post

Get details of a specific post.

Params: postId

update_post

Update a draft or scheduled post.

Params: postId, content?, scheduledAt?

delete_post

Delete a post with provider-safe compensation. Provider-bound and reconciliation rows require strictCompensation; platformPostId may be supplied only as an exact persisted-ID check.

Params: postId, strictCompensation?, platformPostId?

retry_post

Repair or retry a failed single post. Provider-confirmed rows are repaired locally with no republish; uncertain rows reconcile or fail closed. Failed threads without a provider ID require operator reconciliation or creation of a new thread.

Params: postId

Calendar & Analytics

get_calendar

Get a content calendar view with posts, gaps, and summary stats.

Params: startDate, endDate, accountId?, platform?

get_post_analytics

Get engagement metrics (likes, comments, shares, impressions) for a post.

Params: postId

get_analytics_summary

Get aggregate analytics across all posts for a date range.

Params: startDate, endDate, platform?, accountId?

Threads & Links

create_thread

Create a multi-part thread (Twitter thread or Instagram carousel). Instagram aiGenerated requests fail closed unless verified native-disclosure access is enabled.

Params: accountId, items[], scheduledAt?, aiGenerated?

generate_tracked_link

Generate a UTM-tagged URL for tracking social media traffic.

Params: url, platform?, campaign?, content?, postId?, save?

Engagements

get_engagements

List stored engagements (comments/replies) with optional filters.

Params: postId?, isRead?, limit?, cursor?

reply_to_engagement

Reply to a comment/reply directly on the social platform.

Params: engagementId, content

list_unread_engagements

List all unread engagements (inbox shortcut).

Params: limit?

Content

repurpose_content

Repurpose content for different platforms using AI. Supports preview mode (returns variants) and post mode (adapts and publishes). Content is humanized automatically.

Params: sourceContent, targetPlatforms[], mode?, tone?, accountIds?, mediaUrls?, appendContent?, appendToAll?

A/B Testing & Timing

create_ab_test

Atomically create an idempotent A/B test with content variants for an account. Reuse the exact key after a timeout.

Params: idempotencyKey, accountId, name, variants[], metric?, minDurationHours?

get_ab_test_results

Get A/B test results with per-variant analytics and winner.

Params: testId

get_best_posting_times

Get recommended posting times based on historical engagement data.

Params: accountId, timezone?

get_optimal_slot

Find the best available time slot for a new post based on calendar gaps and engagement data.

Params: accountId, startDate?, endDate?

auto_schedule_week

Idempotently fill a week with optimally timed posts. A durable manifest preserves deterministic post IDs, slots, quota reservations, and partial outcomes across same-key retries. Each post can request a native AI-content label; Instagram requests fail closed unless verified native-disclosure access is enabled.

Params: idempotencyKey, accountId, posts[], timezone?

Hermes Agent Integration

Hermes Agent (Nous Research) ships a first-class MCP client. Point it at the published @socialcannon/mcp package and all 23 tools load over npx — no repo clone.

Add to ~/.hermes/config.yaml
mcp_servers:
  socialcannon:
    command: "npx"
    args: ["-y", "@socialcannon/mcp"]
    env:
      SOCIALCANNON_CLIENT_ID: "your-client-id"
      SOCIALCANNON_CLIENT_SECRET: "your-client-secret"

Use the SOCIALCANNON_CLIENT_ID / SOCIALCANNON_CLIENT_SECRET from your dashboard. Run /reload-mcp in your Hermes session and the tools load automatically (Hermes also auto-reloads config edits within ~30s). Other MCP hosts share the same command, args, and env — Claude Desktop uses an equivalent JSON block (mcpServers) in claude_desktop_config.json.

OpenClaw Integration

SocialCannon is available as an OpenClaw skill. Install it from ClawHub so your AI assistant can publish, schedule, analyze, and manage social media posts through natural conversation.

Install from ClawHub
openclaw skills install socialcannon
Configure credentials

Add your SocialCannon API credentials to your openclaw.json config:

{
  "skills": {
    "entries": {
      "socialcannon": {
        "enabled": true,
        "env": {
          "SOCIALCANNON_CLIENT_ID": "your-client-id",
          "SOCIALCANNON_CLIENT_SECRET": "your-client-secret"
        }
      }
    }
  }
}
SKILL.md reference

If you prefer manual installation, create this file at ~/.openclaw/workspace/skills/socialcannon/SKILL.md:

---
name: socialcannon
description: >
  Publish, schedule, and manage social media posts across
  Twitter/X, Facebook, Instagram, LinkedIn, TikTok, and YouTube.
  Includes content calendar, analytics, A/B testing,
  engagement inbox, AI content repurposing, and timing suggestions.
version: 1.0.0
metadata:
  openclaw:
    requires:
      env:
        - SOCIALCANNON_CLIENT_ID
        - SOCIALCANNON_CLIENT_SECRET
      bins:
        - curl
    primaryEnv: SOCIALCANNON_CLIENT_ID
    emoji: "\U0001F4E3"
    homepage: https://socialcannon.app
---

# SocialCannon

Social media publishing API with 23 MCP tools.

## Authentication

First, get a JWT token:

```bash
curl -X POST https://socialcannon.app/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d "{
    \"grant_type\": \"client_credentials\",
    \"client_id\": \"$SOCIALCANNON_CLIENT_ID\",
    \"client_secret\": \"$SOCIALCANNON_CLIENT_SECRET\"
  }"
```

Use `response.data.access_token` (responses are wrapped in `{ success, data }`) as a Bearer token for all
subsequent requests. Tokens expire after 1 hour — request a new one when you get a 401.

## Core Operations

- POST /api/v1/posts — Create/publish a post
- GET /api/v1/posts — List posts (cursor pagination)
- POST /api/v1/posts/thread — Create thread/carousel
- GET /api/v1/calendar — Content calendar with gaps
- GET /api/v1/posts/:id/analytics — Engagement metrics
- GET /api/v1/engagements — Comment inbox
- POST /api/v1/engagements/:id/reply — Reply to comment
- POST /api/v1/posts/repurpose — AI content adaptation
- POST /api/v1/ab-tests — Create A/B test (requires stable Idempotency-Key header)
- GET /api/v1/accounts/:id/timing — Best posting times
- POST /api/v1/links/generate — UTM-tagged URL

Bootstrap & Setup

Create your first API client:

npx tsx scripts/create-client.ts "My App"

This writes directly to Firestore and prints the client_id andclient_secret. Save the secret — it's only shown once.

Or use the dashboard:

Sign in with Google or GitHub at /sign-in and an API client is automatically provisioned for your account.

Stuck on something? Reach out at support@socialcannon.app