Blog

Social Sharing API: One Publish Call Across Every Network (2026)

Erwan Prost

Erwan Prost

· 16 min read

On this page

A social sharing API turns one logical post into nine platform transactions. The mechanical work is the same every time: normalize the text and media, get the bytes somewhere the platform can read them, stage the post, publish, confirm. What changes per network is how many round trips each of those steps costs. Instagram and Threads want a container created and then published. LinkedIn takes a single POST with two mandatory headers. X wants an upload first, then a create.

The first thing that breaks is the caption. A 400-character caption is legal on Instagram, which documents a 2,200-character ceiling, and illegal on both Threads at 500 and X at 280 weighted characters. Nothing warns you in advance. You find out one target at a time, at publish time, which is how a naive fan-out gets expensive long before it gets correct.

What one publish call has to do on nine networks

Fan-out is the easy part. Nine HTTP clients is a weekend of work. What costs a quarter is that the networks disagree about what a post is: Instagram refuses a text-only post, X refuses a fifth image, LinkedIn refuses an organic carousel outright, and YouTube models the whole thing as a video resource with a title and a description instead of a caption.

  1. 1.Normalize the content. One text field becomes caption on Instagram, commentary on LinkedIn, title on TikTok and YouTube, and text on Threads. The field names are trivia. The counting rules behind them are not.
  2. 2.Move the media. Meta downloads from an image_url you host, TikTok and X and LinkedIn want the bytes pushed to them first, and YouTube runs a resumable upload. Three transfer models collapsed into one media_ids array on your side.
  3. 3.Stage the post. Instagram and Threads create a container that exists without being live, and an unpublished Threads container expires after 24 hours. LinkedIn and X skip staging entirely.
  4. 4.Publish, then confirm. A 201 from LinkedIn means published. A container id from Meta means nothing until the second call returns, and a video container can still be IN_PROGRESS minutes later.

Then comes the part nobody designs for on the first pass: partial success. Three targets, two published, one rejected for a caption forty characters too long. No HTTP status tells that story honestly. 200 is a lie and 400 is a worse one, so the per-target verdict has to live in the body and your caller has to read it.

bash
curl -X POST https://api.social-api.ai/v1/posts \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "targets": [
      { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J" },
      { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W2M" },
      { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W3N" }
    ],
    "text": "Shipped cross-platform publishing today.",
    "media_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"]
  }'

# {
#   "id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
#   "status": "partial",
#   "targets": [
#     { "platform": "instagram", "status": "published",
#       "platform_post_id": "17899..." },
#     { "platform": "linkedin",  "status": "published",
#       "platform_post_id": "urn:li:share:6844785523593134080" },
#     { "platform": "x",         "status": "failed",
#       "error": { "code": "content.text_too_long", "limit": 280 } }
#   ]
# }

Two design decisions fall out of that shape. Post state is readable with GET /v1/posts/{id} at any point, because the platforms that stage content report progress on their own schedule, and the posts webhook family pushes the verdict once a target settles. And a failed target is retryable on its own through social_api_retry_post, so fixing one caption does not republish the two targets that already went live.

What a publish call does per target
WHAT A PUBLISH CALL DOES PER TARGETValidateText, media,and rules per…Upload mediaPublic URL,chunked bytes…Create containerMeta only:staged, not yet…PublishOne call pertarget, indep…ConfirmPoll status,retry failed…Validate hits no platform APIFailures are per target

Validation is the only stage that touches no platform API. Everything after it costs a network round trip, and every stage after upload can fail on one target while the others succeed.

The stage worth arguing about is the first one. Validation touches no platform API at all, which makes it the only step in that pipeline that can fail for free.

Check the rules before you publish, not after

SocialAPI.ai exposes the per-platform rules as data rather than as documentation. social_api_get_constraints returns what a given target will accept. social_api_validate_post runs a draft against every target you name and returns all the violations together. One request, nine verdicts. The alternative is the loop everyone writes first: publish, catch a 400, fix, publish again, catch the next platform's 400.

json
// social_api_validate_post
{
  "targets": [
    { "account_id": "acc_...ig" },
    { "account_id": "acc_...threads" },
    { "account_id": "acc_...x" }
  ],
  "text": "<412 characters of launch copy>",
  "media_ids": ["f47ac10b", "58cc4372", "a5670e02", "b2c3d479", "9b1deb4d"]
}

// Response: every violation, before a single platform call.
{
  "valid": false,
  "violations": [
    { "platform": "threads", "field": "text",
      "code": "text_too_long", "limit": 500, "actual": 412,
      "severity": "ok" },
    { "platform": "x", "field": "text",
      "code": "text_too_long", "limit": 280, "actual": 412 },
    { "platform": "x", "field": "media_ids",
      "code": "too_many_media", "limit": 4, "actual": 5 }
  ]
}

That collapses four sequential discoveries into one response, and it is the reason the rules live behind an endpoint instead of a table in our docs. Limits move. Instagram's carousel cap and the Threads 500-character ceiling are stable, but TikTok's video ceiling is 3, 5, or 10 minutes depending on the creator account you are posting to, and no static table can answer that for a specific account.

Per-platform constraints that break a naive share API

Every row below comes from that platform's own developer documentation, read on 4 August 2026, with all sources linked at the end of this post. Where a cell reads Not stated, the platform's own reference publishes no number for it. That does not mean no limit exists. It means you cannot write code against one. Telegram is absent because its Bot API sends chat messages rather than feed posts, so the fields do not line up.

PlatformMedia per postAspect ratioText limitMulti-imageVideo
Instagram1, or carousel up to 10Image 4:5 to 1.91:1; Reels 0.01:1 to 10:12,200 chars, 30 hashtags, 20 @ tags10 max, images and video mixed, counts as one postReels 3s to 15 min
Facebook PageNot stated; link cards 2 to 5Reels 9x16; feed not statedNot statedNot stated for media postsReels 3s to 90s; feed not stated
Threads1, or carousel 2 to 20Image up to 10:1, width 320 to 1440 px500 chars2 to 20, images and video mixed300s max, 1 GB
TikTokUp to 35 photos (photo mode)Not stated; video 360 to 4096 pxVideo title 2,200 UTF-16 runes; photo title 90Up to 35 photo URLs; minimum not stated10 min via API, 4 GB
YouTube1 videoNot statedtitle 100 chars; description 5,000 bytesNot applicableDuration not stated; 256 GB
X / Twitter4 photos, or 1 GIF, or 1 videoVideo 1:3 to 3:1; image not stated280 weighted chars (URL 23, emoji 2)No carousel; 4 photos, no mixing0.5s to 140s, 512 MB
LinkedIn1, or MultiImage 2 to 20Not stated; image max 36,152,320 pxNot stated (FIELD_LENGTH_TOO_LONG)MultiImage 2 to 20, images only; organic carousel unsupported3s to 30 min, 75 KB to 500 MB
Google Business ProfileNot statedNot stated; photo min 250 px short edgeNot statedNot statedNot stated

Three failure classes hide in that grid. There are hard numeric caps you can check locally before sending anything (2,200, 500, 280). There are structural rules that reject an entire content type, like LinkedIn refusing an organic carousel or Instagram refusing a post with no media. And there is per-account variation, TikTok's video ceiling being the clearest case, which only the account itself can answer.

The blank cells are more dangerous than the small numbers. Meta's Page feed reference describes the message parameter without ever giving it a length. LinkedIn's Posts API documents no commentary limit either, and instead lists a FIELD_LENGTH_TOO_LONG error reading {field} length exceeds the allowed maximum. You discover that maximum by hitting it, in production, on a customer's account.

Text length is not even one measurement. Instagram counts characters. YouTube counts bytes, so an emoji-heavy description hits 5,000 sooner than its character count suggests. TikTok counts UTF-16 runes. X counts weighted characters, where every URL costs 23 regardless of length and every emoji costs 2. Four counting systems for one question, and text.length agrees with exactly one of them.

Four platforms, four ways to measure the same sentence. A naive text.length agrees with exactly one of them.

Multi-image support disagrees on every axis available. Instagram allows 10 items, mixes images with video, and counts the whole set as a single published post. Threads allows 2 to 20 with the same mixing. TikTok's photo mode takes up to 35 URLs. X caps at 4 photos and forbids mixing them with a GIF or a video. LinkedIn allows 2 to 20 images and no video among them.

Three media transfer models, one media_ids field

Media transfer is where the abstraction leaks hardest, because the three models fail in incompatible places.

  • Meta fetches. You pass image_url or video_url and Meta downloads it, so the file has to sit on a publicly reachable host before the first call. A 403 from your own CDN surfaces as a container error minutes later, not as a request error.
  • TikTok, X, and LinkedIn ingest. You initialize an upload, push bytes (TikTok wants chunks of 5 MB to 64 MB, up to 1,000 of them), then reference the id you get back. Network failures land mid-transfer instead of at publish.
  • YouTube resumes. videos.insert accepts up to 256 GB through a resumable upload, so retries are scoped to a byte range rather than to a whole request.

SocialAPI.ai flattens all three behind social_api_get_media_upload_url and social_api_verify_media_upload. You upload once, get a media id, and that id is valid for every entry in the targets array regardless of which transfer model the platform underneath requires. The content publishing API covers that surface end to end.

Posting a photo through the Instagram Graph API

Two calls, and the first one publishes nothing. POST /{ig-user-id}/media with image_url and caption returns a container id. POST /{ig-user-id}/media_publish with that id as creation_id makes the post live. Meta downloads the image itself during the first call, which is the most common source of confusion here: the file has to be reachable from Meta's servers, not from your laptop.

# 1. Create the media container. Meta downloads image_url itself.
CONTAINER=$(curl -s -X POST \
  "https://graph.facebook.com/v23.0/$IG_USER_ID/media" \
  -d "image_url=https://cdn.example.com/launch.jpg" \
  -d "caption=Shipped cross-platform publishing today." \
  -d "access_token=$IG_TOKEN" | jq -r .id)
# { "id": "17895695668004550" }

# 2. Publish the container.
curl -s -X POST \
  "https://graph.facebook.com/v23.0/$IG_USER_ID/media_publish" \
  -d "creation_id=$CONTAINER" \
  -d "access_token=$IG_TOKEN"
# { "id": "17899..." }

# Rejected media does NOT fail on step 1. Read it back:
curl -s "https://graph.facebook.com/v23.0/$CONTAINER\
?fields=status_code,status&access_token=$IG_TOKEN"

Three documented limits govern that call. The caption tops out at 2,200 characters, 30 hashtags, and 20 @ tags. Feed images must fall between 4:5 and 1.91:1, and a carousel crops every item to the first image's ratio, defaulting to 1:1. A 10-item carousel spends exactly one publish against the 24-hour ceiling.

That ceiling is stated twice, differently, on Meta's own page: "Instagram accounts are limited to 100 API-published posts within a 24-hour moving period", and further down, "Accounts are limited to 50 published posts within a 24-hour period". Budget against 50. Planning low costs nothing if the true ceiling turns out to be 100, while a scheduler built on 100 quietly runs dry at midday if it does not.

Failures surface in the wrong place, which is the part that costs debugging hours. The create call hands back a container id cheerfully, and the reason a video was rejected only appears on a later status read. Threads runs the same container model with different numbers, and the Threads publishing guide walks that flow end to end.

Posting content through the LinkedIn API

LinkedIn has no container step. One POST https://api.linkedin.com/rest/posts creates the post, returns 201, and puts the new id in the x-restli-id response header rather than in the body, which trips up anyone who parses only JSON. What LinkedIn does have is a versioning discipline stricter than anything else on this list.

bash
curl -X POST 'https://api.linkedin.com/rest/posts' \
  -H "Authorization: Bearer $LI_TOKEN" \
  -H 'X-Restli-Protocol-Version: 2.0.0' \
  -H 'LinkedIn-Version: 202607' \
  -H 'Content-Type: application/json' \
  --data '{
    "author": "urn:li:organization:5515715",
    "commentary": "Shipped cross-platform publishing today.",
    "visibility": "PUBLIC",
    "distribution": {
      "feedDistribution": "MAIN_FEED",
      "targetEntities": [],
      "thirdPartyDistributionChannels": []
    },
    "content": { "media": { "id": "urn:li:image:C49klciosC89" } },
    "lifecycleState": "PUBLISHED",
    "isReshareDisabledByAuthor": false
  }'

# HTTP/1.1 201 Created
# x-restli-id: urn:li:share:6844785523593134080
# (empty body)

Both headers are mandatory. X-Restli-Protocol-Version: 2.0.0 never changes; LinkedIn-Version takes a YYYYMM string that does. LinkedIn ships a marketing version monthly and sunsets the old ones, and the July 2025 version is already sunset, so a client that pins a version and forgets about it stops working on LinkedIn's calendar rather than yours. Every entity is a URN: urn:li:organization:5515715 as the author, urn:li:image:{id} as the content.

Mentions carry their own trap. @[Devtestco](urn:li:organization:2414183) renders as a link only when the display text matches the entity name exactly, case sensitive, and silently degrades to plain text when it does not. Multiple images go through a separate MultiImage content type: 2 to 20 images, no video among them. Video posts run 3 seconds to 30 minutes, between 75 KB and 500 MB.

What happens when one target fails

Failure is normal here, not exceptional. A token expires, an aspect ratio gets rejected, one network rate-limits you while the other eight sit idle. SocialAPI.ai keeps per-target state so the post stays the unit of intent while the target stays the unit of delivery. social_api_retry_post re-runs only the targets that failed and leaves the published ones untouched.

bash
# Read per-target state back. Or subscribe to post.published / post.failed.
curl https://api.social-api.ai/v1/posts/post_01HZ9X3Q4R5M6N7P8V2K0W1J \
  -H "Authorization: Bearer $SOCAPI_KEY"

# {
#   "id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
#   "status": "partial",
#   "targets": [
#     { "platform": "instagram", "status": "published" },
#     { "platform": "linkedin",  "status": "published" },
#     { "platform": "x", "status": "failed",
#       "error": { "code": "content.text_too_long", "limit": 280 } }
#   ]
# }
#
# Fix the text, then re-run the failed target alone with
# social_api_retry_post. The two published targets are untouched.

On the inbound side the retry policy is documented rather than implied: 5 attempts at immediate, roughly 30 seconds, 5 minutes, 30 minutes, and 3 hours, after which the delivery is marked failed and can be replayed once your endpoint is fixed. Anything a retry can duplicate, your handler has to deduplicate.

The events split into two families. The inbox family is six events (comment.received, dm.received, dm.sent, dm.referral, mention.received, review.received), separate from the publishing lifecycle events in the posts category. Publishing work lives in the second one: post.published fires when every target succeeded, post.failed when none did. GET /v1/webhooks/events returns the authoritative catalog of both.

Questions developers ask about social sharing APIs

What is a social sharing API?
A social sharing API is a single HTTP interface that publishes one piece of content to several social networks, handling the per-platform differences on your behalf. Mechanically it does four things per target: it maps your fields onto that platform's field names, moves the media using whichever transfer model the platform requires, stages the post if the platform demands a staging step, then publishes and reports per-target status. The value is not the fan-out itself, which is straightforward, but the normalization of nine incompatible media models, text-length rules, and error vocabularies into one contract.
How do you post to multiple social networks with one API call?
Send one request with a targets array and let the provider expand it. On SocialAPI.ai that is POST /v1/posts with targets, text, and media_ids, and the response carries a per-target status because targets fail independently. Expect partial success: a caption legal on Instagram at 2,200 characters is rejected by X at 280, so the honest design puts the verdict per target in the response body rather than in the HTTP status. Validate first with social_api_validate_post if you want the violations before any platform call is made.
How do you post a photo with the Instagram Graph API?
In two calls. Send POST /{ig-user-id}/media with image_url and caption to create a media container; Meta downloads the image from that URL itself, so it must be on a publicly reachable host. The call returns a container id. Then send POST /{ig-user-id}/media_publish with creation_id set to that id, and the post goes live. Captions allow 2,200 characters, 30 hashtags, and 20 @ tags. Feed images must be between 4:5 and 1.91:1. A carousel of up to 10 items counts as a single published post. Meta's content publishing page states the 24-hour ceiling twice and inconsistently, once as 100 API-published posts and once as 50, so budget against 50 and treat the higher number as headroom you have not confirmed.
How do you post content through the LinkedIn API?
Send POST https://api.linkedin.com/rest/posts with two mandatory headers, X-Restli-Protocol-Version: 2.0.0 and LinkedIn-Version in YYYYMM format, plus a body containing author (an organization or person URN), commentary, visibility, distribution, and lifecycleState set to PUBLISHED. A successful create returns 201 with the new post id in the x-restli-id response header, not in the body. Images and videos must be uploaded first through the Images API or Videos API to obtain a urn:li:image or urn:li:video to reference. LinkedIn sunsets marketing API versions on a monthly cadence, so a pinned version needs an owner.
What is the character limit for a social media post?
Write to the shortest limit among the targets you publish to, because the unit varies as much as the value. Instagram allows 2,200 characters in a caption with 30 hashtags and 20 @ tags. Threads allows 500 characters. X allows 280 weighted characters, where any URL counts as 23 and any emoji counts as 2. TikTok allows 2,200 UTF-16 runes in a video title and 90 in a photo-post title. YouTube allows 100 characters in a title and 5,000 bytes in a description. Facebook Page posts, LinkedIn commentary, and Google Business Profile summaries have no number published in their own developer documentation, so those three cannot be checked before sending.
What happens when one platform in a multi-target post fails?
Nothing should roll back, because the successful targets are already live on networks you do not control. SocialAPI.ai marks the post partial, records the failing target with a typed error code and the limit that was exceeded, and leaves the published targets alone. You fix the offending field and call social_api_retry_post for that target only. Publishing state is readable with GET /v1/posts/{id} at any point, and the posts webhook family (post.published, post.partial, post.failed) pushes the verdict once every target settles.
Do you need separate app approval for each network to publish?
For most of them, yes, if you build the integration yourself. Instagram, Facebook, and Threads require Meta App Review per permission; TikTok requires a client audit before content is publicly visible; YouTube and Google Business Profile require Google verification; LinkedIn grants publishing scopes through its Products and Partner Programs. Connecting through a provider whose platform apps already passed review replaces those submissions with an OAuth redirect, at the cost of inheriting that provider's standing with each platform.

To publish across nine networks without maintaining nine payload builders, read the API reference, or validate a draft against every target with social_api_validate_post before it ships. If you are still weighing whether to write the connectors in-house, social media API integration has the arithmetic, and tell us your platform mix if you want a straight answer about your specific set.

Primary platform documentation behind the constraint table, all accessed 4 August 2026: Meta: Instagram content publishing (carousel of 10; the same page states both 100 and 50 posts per 24h) · Meta: Instagram user media reference (2,200-char caption, 4:5 to 1.91:1, Reels 3s to 15 min) · Meta: Threads posts (500 characters, carousel 2 to 20, 300-second video) · Meta: Page feed reference (child_attachments 2 to 5, no message length published) · Meta: Facebook Reels publishing (9x16, 3 to 90 seconds) · TikTok: Content Posting API direct post (2,200 UTF-16 runes) · TikTok: Content Posting API photo post (up to 35 photos, 90-rune title) · TikTok: media transfer guide (10-minute API ceiling, 4 GB, 5 to 64 MB chunks) · YouTube Data API: videos resource (100-char title, 5,000-byte description) · YouTube Data API: videos.insert (256 GB maximum file size) · X: media best practices (4 photos, video 1:3 to 3:1, 0.5 to 140 seconds, 512 MB) · X: counting characters (280 weighted, URL 23, emoji 2) · X: creation of a post (media_ids maximum of 4) · LinkedIn: Posts API (required headers, x-restli-id, FIELD_LENGTH_TOO_LONG, no organic carousel) · LinkedIn: MultiImage post API (2 to 20 images, images only) · LinkedIn: Images API (36,152,320-pixel ceiling, 4,086-char altText) · LinkedIn: Videos API (3 seconds to 30 minutes, 75 KB to 500 MB) · Google Business Profile: local posts reference (no published field limits) · Google Business Profile: media reference (250 px short edge minimum)

Get started today

Ready to unify your social interactions?

Free tier available · No credit card required · Ships with MCP server

We use essential cookies for security, and analytics cookies with your consent. Privacy Policy.