Developer docs
Quality

Publishing per channel

What each adapter actually calls — who moves the bytes, the container dances, and the failures worth recognising.

docs/CHANNEL_PUBLISH.md

POST_FLOW.md ends where the publisher hands a variant to its adapter. This is what happens after that, and it is different on every channel.

One split explains most of the differences.


Who moves the bytes

PatternChannels
Pull — platform fetches our URLInstagram · Facebook · Threads · Pinterest · TikTok
Push — we upload the fileLinkedIn · YouTube

This is not trivia. Every pull channel fails silently on a local machine, because localhost:4000 is not reachable from Meta's servers. That is one of the two reasons a publish that looks fine in dev fails in production; the other is that a pull channel's error arrives later, from a fetcher, not from the call you made.

It also decides where a slow upload hurts. On push channels the publish request carries the file and can take minutes; on pull channels the call returns immediately and the waiting happens somewhere you cannot see.


Instagram — three calls, and the middle one matters

The container is not ready when it is created — Meta downloads and transcodes asynchronously. Publishing too early returns 9007 / 2207027, "media is not ready for publishing", which reads like a caller bug and is really a race.

Post types map to media_type: STORIES for a story, REELS for a reel or any video, CAROUSEL for 2–10 items (each child created first with is_carousel_item=true), and a plain image_url otherwise. Stories are single-media — refused locally rather than at the API, whose error names neither file.


Facebook — the edge depends on the media

ContentCall
1 imagePOST /{page}/photos with url
2–10 imageseach to /photos with published=false, then POST /{page}/feed with attached_media[n]
videoPOST /{page}/videos
text or linkPOST /{page}/feed

The multi-image path is the interesting one: the photos are uploaded unpublished, and the feed post is what makes them visible as one album. Checking the order here matters — an image check that ran before the video branch once routed videos to /photos.


LinkedIn — register, upload, then post

Two publish endpoints, and picking the wrong one fails loudly. ugcPosts carries text, images, video and articles through shareMediaCategory. It has no vocabulary for a document — sending "DOCUMENT" returns "DOCUMENT" is not an enum symbol (422). A PDF publishes through the versioned /rest/posts API instead, the sibling of the API that minted its URN.

Three differences between those bodies are easy to miss and none survives a copy-paste: commentary replaces shareCommentary.text, a distribution block is mandatory, and visibility is a plain string rather than a nested map — the map is accepted and silently ignored, which would publish a connections-only post to the whole feed while looking like success.


YouTube — a resumable upload

Video only. Google's resumable protocol: start a session, send 8 MiB chunks, resync from the Range header if a chunk fails, retry 5xx with a status probe.

The real ceiling is not technical — an unaudited app forces every upload to private, and the 10,000-unit daily quota caps at roughly six publishes.


Threads, Pinterest, TikTok, X

ChannelShapeNotes
Threadscontainer → POST /me/threads_publishtext and image only; media_type: TEXT or IMAGE. Video is not supported by Meta yet
PinterestPOST /v5/pins with media_source: { source_type: "image_url" }image only; needs a board
TikTokvideo/init or content/init with PULL_FROM_URL → poll publish/status/fetchsame container-then-poll shape as Instagram. One image only for now
XPOST /2/tweetstext works; image and carousel are stubbed. Media needs v1.1 media/upload with OAuth 1.0a HMAC signing, which the OAuth 2.0 flow here does not do

What each supports

Channeltextimagecarouselvideoreelstorydocument
Instagram—✅✅✅✅✅—
Facebook✅✅✅✅———
LinkedIn✅✅✅✅——✅
YouTube———✅———
Threads✅✅—————
Pinterest—✅—————
TikTok—✅—✅———
X✅⚠️ stub⚠️ stub————

The composer offers a type only where the selected channels support it — which is why PDF appears for LinkedIn alone.


Every published row stores the link the platform returned, so published is checkable rather than asserted. derivePermalink returns null rather than constructing a URL it cannot verify: Instagram uses a shortcode unrelated to the media id, so its link has to be fetched, and a guessed URL that 404s is worse than no link at all.


Failures worth recognising

What you seeChannelCause
9007 / 2207027 media is not readyInstagrampublished before the container finished
"DOCUMENT" is not an enum symbolLinkedIna PDF sent to ugcPosts instead of /rest/posts
Works in dev, fails in prodany pull channelthe media URL is not reachable from the internet
(#10) Application does not have permissionMetathe scope was never granted to the token in hand
Publishes but only to youYouTubeunaudited app — every upload is forced private
Image silently absentXmedia is stubbed; the text posts without it