# Mellow Hub Publish one post to every network you are connected to — Instagram, TikTok, YouTube, X, LinkedIn, Threads, Bluesky, Pinterest and Facebook — through one API call, with each network's own rules checked before anything goes out. Base URL: `https://www.mellow.world/api/hub/v1` MCP endpoint: `https://www.mellow.world/mcp` ## The shape of the work 1. **`whoami`** — what this credential may do, which channels it can reach, and how many posts are left today. Call it first; everything else depends on the answer. 2. **`list_channels`** — the connected accounts. Each has an `id` beginning `spc_`; that is what you name in `channels`. 3. **`register_media`** — if the post has media. Any public https URL works and nothing is copied: the network fetches it when it publishes. For bytes you hold, ask for an upload URL and send the file straight there. 4. **`validate_post`** — free, publishes nothing, and returns every problem at once. Do this before `create_post` every time. It is the difference between fixing a post once and discovering the rules by breaking them publicly. 5. **`create_post`** — publishes now, or schedules for later. 6. **`get_post`** — the result per channel, with the public link. ## Connecting a channel You cannot do this. Every network makes a person sign in, and Bluesky asks for a handle and an app password typed into Mellow. `connect_channel` returns a URL and a sentence explaining who has to open it — pass both on and wait. When the person is done, `list_channels` shows the new channel. ## Writing one post for many networks A post has one caption, one media list, and a list of channels. Where a network genuinely differs, say so in `options`, keyed by platform: ```json { "caption": "The same thought, everywhere.", "media": ["https://cdn.example.com/clip.mp4"], "channels": ["spc_instagram", "spc_youtube", "spc_bluesky"], "options": { "instagram": { "placement": "reels" }, "youtube": { "title": "The same thought", "privacyStatus": "public" } }, "perChannel": { "spc_bluesky": { "caption": "The same thought, in 300 characters." } } } ``` `options` applies to every channel of that network. `perChannel` applies to one channel and wins over `options` — it is how you give Bluesky a shorter caption without shortening the post everywhere. ## Publishing twice is the failure to avoid `create_post` requires an idempotency key, and it is not decoration. If your request times out you do not know whether it published; retrying with the same key returns the original post instead of making a second one. Use a key derived from the content or the task, not a random value per attempt. Reusing a key for *different* content is refused, because that is a bug worth seeing. ## Scheduling Give `scheduledAt` an ISO 8601 timestamp to publish later; omit it to publish now. Cancelling works while a post is still scheduled. Once a network has published there is no unpublishing it through any API — Mellow will say so rather than pretend. ## What comes back Each channel succeeds or fails on its own. A post to five networks where YouTube rejects the title and the other four publish comes back as `partial`, with the failure named on the YouTube channel and public links on the rest. Read `targets`, not just `status`. ## Authority and limits A key is a delegation with edges: the channels it may touch, whether it publishes on its own (`autopilot`) or only prepares a post for a person to approve (`review`), and how many posts it may make in a day. `whoami` reports all of them. Everything done with the key is recorded in a journal the owner reads, and revoking a key takes effect immediately. ## Reading further - `mellow://guide/platforms` — every network's limits and required fields. - `mellow://guide/errors` — what each refusal means and what to do about it. - `mellow://guide/recipes` — worked examples for the common jobs. --- # Every network, and what it asks for These are the rules Mellow checks before it sends anything. A post that passes `validate_post` has already satisfied everything on this page; what remains is the network's own judgement about the file itself — resolution, duration, aspect ratio — which no API can answer in advance. ## Instagram (`instagram`) **Connecting.** A professional (Business or Creator) Instagram account. A personal account cannot publish through any API. Needed to start the connection: - `connection_type` — Instagram login is the direct route and the default. Facebook login is for accounts reached through a Page. One of: `instagram`, `facebook`. **Caption.** Up to 2200 characters. **Media.** At least 1, at most 10; image or video. Stories take exactly one item. Reels take one video. **Placement.** `timeline`, `reels`, `stories`. **Supports.** schedule, carousel, thumbnail, collaborators, mediaTags, metrics. ## TikTok (`tiktok`) **Connecting.** A personal or creator TikTok account. **Caption.** Up to 2200 characters. **Title.** Optional, up to 90 characters. Used as the headline on photo posts. **Media.** At least 1, at most 35; image or video. One video, or up to 35 images as a photo post. Videos and images cannot be mixed. **Supports.** schedule, draft, carousel, metrics. ## TikTok Business (`tiktok_business`) **Connecting.** A TikTok Business account. Use it when you need business analytics; otherwise the ordinary TikTok connection is simpler. **Caption.** Up to 2200 characters. **Title.** Optional, up to 90 characters. **Media.** At least 1, at most 35; image or video. One video, or up to 35 images as a photo post. **Supports.** schedule, draft, carousel, thumbnail, metrics. ## YouTube (`youtube`) **Connecting.** A Google account with a YouTube channel. Uploads count against the channel's daily API quota. **Caption.** Up to 5000 characters. Becomes the video description when no explicit description is given. **Title.** Required, up to 100 characters. YouTube refuses an upload without a title. **Media.** At least 1, at most 1; video. Exactly one video. A vertical video under three minutes is published as a Short by YouTube itself — there is no separate Shorts endpoint to call. **Required options.** `title`. **Supports.** schedule, thumbnail, metrics. ## X (`x`) **Connecting.** Posting through the X API depends on the access tier of the app the connection is made with. Needed to start the connection: - `connection_type` — OAuth 2.0 is the default. OAuth 1.0a exists for apps still on the older credentials. One of: `oauth2`, `oauth1`. **Caption.** Up to 280 characters. 280 is the limit for a standard account. A premium account may write far more, but Mellow cannot see the account's tier, so anything longer is reported rather than silently sent. **Media.** Optional, at most 4; image or video. Up to four images, or one video. Text on its own is fine. **Supports.** schedule, carousel, poll, metrics. ## LinkedIn (`linkedin`) **Connecting.** You need admin rights on the company page you want to publish to. Needed to start the connection: - `connection_type` — A company page is the route available on the provider's shared credentials. A personal profile needs Mellow's own approved LinkedIn app. One of: `organization`, `personal`. **Caption.** Up to 3000 characters, and required. LinkedIn publishes nothing without commentary. **Media.** Optional, at most 20; image or video. Up to twenty images, or one video. **Supports.** schedule, carousel, metrics. ## Threads (`threads`) **Connecting.** The Threads account attached to your Instagram login. **Caption.** Up to 500 characters. **Media.** Optional, at most 20; image or video. Text on its own is fine. Up to twenty items in a carousel. **Placement.** `timeline`, `reels`. **Supports.** schedule, carousel, metrics. ## Bluesky (`bluesky`) **Connecting.** Bluesky has no OAuth consent screen: the connection is made with a handle and an app password, so an agent cannot complete this step alone. Needed to start the connection: - `handle` — Your full Bluesky handle, for example name.bsky.social. - `app_password` — Create one in Bluesky under Settings → App Passwords. Never your account password — an app password can be revoked on its own. **Caption.** Up to 300 characters, and required. Bluesky counts graphemes, so emoji and combined characters can cost more than they look. **Media.** Optional, at most 4; image or video. Up to four images, or one video. **Supports.** schedule, carousel, metrics. ## Pinterest (`pinterest`) **Connecting.** A Pinterest business account with at least one board to pin to. **Caption.** Up to 500 characters. Becomes the pin description. **Title.** Optional, up to 100 characters. **Media.** At least 1, at most 1; image or video. One image or one video per pin. **Supports.** schedule, metrics. ## Facebook (`facebook`) **Connecting.** A Facebook Page you administer. Personal timelines cannot be published to through the API. **Caption.** Up to 63206 characters. **Media.** Optional, at most 10; image or video. Stories take exactly one item. Reels take one video. **Placement.** `timeline`, `reels`, `stories`. **Supports.** schedule, carousel, thumbnail, collaborators, mediaTags, metrics. --- # What a refusal means Every error carries a `code`, a sentence, and `retryable`. Trust `retryable`: if it is false, the same request will fail the same way for ever, and the thing to change is the request. A rejected post also carries `issues`, one per problem, each naming the channel and the field. Fix them together and try once, rather than one at a time. ## Before anything is sent | Code | Meaning | What to do | | --- | --- | --- | | `authentication_required` | No credential, or one Mellow does not recognise. | Present the key or token. | | `insufficient_scope` | The credential lacks a permission this call needs. | The owner grants it when they create the key. | | `channel_not_connected` | That channel ID is not one of this owner's. | Call `list_channels`; IDs are stable but connections come and go. | | `channel_out_of_scope` | The channel exists, but this key was not given it. | Ask the owner, or publish to the channels you do have. | | `caption_too_long` | The caption exceeds that network's limit. | Give the channel a shorter caption with `perChannel`. | | `title_required` | YouTube will not accept an upload without a title. | Set `options.youtube.title`. | | `option_required` | Something the network insists on is missing. | The message names it. | | `media_kinds_mixed` | Images and video in one post. | Split the post, or give the channel its own media. | | `media_required` | The network publishes nothing without media. | Add media, or drop that channel. | | `schedule_in_past` | `scheduledAt` has already passed. | Omit it to publish now. | | `daily_limit_reached` | This key has used its day's posts. | Wait, or the owner raises the limit. | | `idempotency_key_reused` | That key already published something else. | Use a new key. This is a bug in the caller. | ## After it was sent | Code | Meaning | What to do | | --- | --- | --- | | `provider_error` | The publishing provider refused or failed. | Read `retryable`. | | `provider_result_failed` | The network itself rejected this channel's post. | The channel's `errorMessage` carries what the network said. | | `result_never_confirmed` | The network never reported an outcome. | Check the account directly before republishing — it may have gone out. | | `already_published` | Cancelling something already public. | Nothing here can unpublish it; delete it in the network. | ## Two failures worth naming **A partial post is not a failed post.** `status: "partial"` means some channels published. Republishing the whole thing duplicates those. Republish only the channels whose target says `failed`. **A timeout is not a refusal.** If `create_post` times out, the post may exist. Retry with the same idempotency key — that is exactly what it is for — or call `list_posts` and look. --- # Worked examples ## Publish a vertical video everywhere it belongs ```json { "caption": "Three minutes on why we rebuilt the editor.", "media": ["https://cdn.example.com/editor.mp4"], "channels": ["spc_ig", "spc_tiktok", "spc_youtube", "spc_threads"], "options": { "instagram": { "placement": "reels", "shareToFeed": true }, "youtube": { "title": "Why we rebuilt the editor", "privacyStatus": "public" }, "tiktok": { "privacyStatus": "public", "allowComment": true } } } ``` A vertical video under three minutes becomes a Short on YouTube by itself. There is no Shorts setting to send. ## One thought, different lengths Bluesky allows 300 characters and X allows 280; LinkedIn allows 3000 and rewards using them. Write the long version once and override the short ones: ```json { "caption": "The long version, written for LinkedIn…", "channels": ["spc_li", "spc_bsky", "spc_x"], "perChannel": { "spc_bsky": { "caption": "The short version." }, "spc_x": { "caption": "The short version." } } } ``` ## Schedule a week Make one `create_post` per slot with `scheduledAt`, and give each a stable idempotency key — `week-32-tue-morning` rather than a random value — so re-running the plan does not double-post. ## Prepare something for a person to approve Under a `review` key, `create_post` prepares the post and stops. Tell the person it is waiting; they approve it in Mellow. `whoami` says which mode the key is in, so check before promising to publish. ## Recover from a partial failure ``` get_post → status "partial" → targets[2].status "failed", errorMessage "…" ``` Fix what the message names, then `create_post` again with **only the failed channel** and a new idempotency key. Do not resend the channels that published.