Skip to main content
A post publishes content to one or more platforms connected to a profile. You send one request; MADIAD Hub fans it out to every platform you list and returns a per-platform result.
Publishing is unlimited on every paid plan — there is no monthly cap on how many posts you send. Usage is still metered, and one request counts as one upload no matter how many platforms it targets: publishing to TikTok, Instagram, and Facebook in a single call records 1 upload, not 3. See Usage to read your current figures.

Choose an endpoint by media type

The photo endpoint uses multipart/form-data for file uploads, or plain JSON when the images come from URLs (photo_urls[] — see Send photos by URL). Text and video use JSON.

Images per post

MADIAD Hub doesn’t impose its own cap on how many photos you attach to photos[] — it passes them straight through. Each platform enforces its own maximum, so the effective limit for a fan-out post is the smallest limit among the platforms you targeted:
These are the platforms’ own published limits, not something MADIAD Hub enforces — they can change on the platform’s side without notice. When in doubt, test with a small batch first.

Fan out with platforms[]

List every target platform. The same content goes to each, unless you override per platform.
One request shares its media across every listed platform. You can override the text per platform (caption → <platform>_caption, below) and pass platform-specific options, but the photos[] / photo_urls[] / video you attach go to all the platforms in that request — there is no per-platform media field. To publish different images or video per platform, send a separate request per platform (each with its own photos[]/video).

Send photos by URL

Instead of uploading the bytes, pass public links with photo_urls[] — MADIAD Hub downloads each image server-side and publishes it exactly as if you had uploaded the file. Prefer this for large images or big batches: the heavy transfer happens on our side, so your request stays tiny and can never trip a request-size error.
  • Each URL must be a public https:// direct image link (a hostname, not an IP address). A share-page link that opens a web page (e.g. a drive viewer) is rejected with a clear error — use the direct file URL. Redirects are followed (up to 5 hops), but every hop must stay https://.
  • Up to 40 URLs per request, 30 MB per image, and 120 MB combined.
  • Also works in multipart: send photo_urls[] as text fields, and mix them freely with photos[] file uploads in one request — on carousels, uploaded files come first, then URL photos, each in send order.

Per-platform overrides

caption is the shared default. Override it for a single platform with a <platform>_caption field — handy for hashtags, mentions, or length limits.
The same prefix pattern applies to other per-platform options, such as instagram_first_comment.
Instagram overrides are published as their own job. When instagram_caption differs from the shared caption and the request also targets other platforms, MADIAD Hub sends Instagram separately so it gets exactly the text you wrote. This is invisible for a normal post — still one upload against your quota, one request_id, one status to poll. For a scheduled post it is visible in one place: the schedule list shows Instagram as its own entry, so cancelling or editing it is a separate action from the other platforms.Google Business photo and video posts work the same way. Their own text (google_business_caption, or the caption cut to 1,500) is sent as a separate Google Business job, because on those post types the platform only reads the shared text. Text posts to Google Business stay in the one job.So do the article platforms: WordPress, Dev.to, Hashnode, Lemmy, Listmonk and Whop. There, <platform>_title is the title of the article and <platform>_caption is its body. A wordpress_caption that differs from the shared caption is sent as a separate WordPress job, so the article carries that text and not the shared one. The add_to_queue rule below applies to them too.One combination is refused: add_to_queue with an instagram_caption that differs from the shared caption, when the request also targets other platforms. Instagram would take its own queue slot and go out at a different time from the rest, so the request is rejected instead — queue Instagram as its own request, or use the shared caption. The same applies to google_business_caption on a photo or video post. Queued on its own, the platform needs no separate job: its caption is used as the post text and queued normally.

Character limits

caption maps to each platform’s main text field. The limit differs per platform — and on a few platforms that field is a short title, with the long text going in a separate description: A few rules follow from the table:
  • A caption too long for a platform is cut for that platform only. Every other platform gets the full caption, and the request is not refused. This holds for titles (YouTube, Pinterest, Reddit, TikTok photo posts) and for bodies (Instagram, LinkedIn, Google Business, TikTok video, Mastodon), and for the descriptions a caption falls back to (Pinterest 500, YouTube 5,000 bytes, TikTok photo 4,000). A title becomes one line; a body keeps its line breaks. Either way the cut lands at the end of a word when that keeps at least 60% of the limit, never splits an emoji, and ends in one ”…”.
  • The response says what was cut. A truncated array lists each platform and text that was cut (text: title, caption or description), with the caption’s length and the limit. Platforms not listed received the full caption. To choose the text yourself, set that platform’s own field: <platform>_caption (or <platform>_title), or the description fields in the table above.
  • Text you set for one platform is never cut. A <platform>_title or <platform>_caption you send is published as written, so one over that platform’s cap is refused with an error naming the field. Description fields you set (description, pinterest_description, youtube_description, tiktok_description) are also sent as written, and are not length-checked before publishing.
  • One exception, queued Instagram (and Google Business or Mastodon photo/video) posts. Instagram’s cut text travels in a separate Instagram job (see the note above), and so does the cut text of Google Business and Mastodon on photo and video posts. So a request with add_to_queue that targets one of them together with other platforms, with a caption over its limit, is refused: shorten the caption, or queue that platform as its own request. Queued on its own, it is cut and queued normally.
  • In the MADIAD Hub dashboard the counter under the caption box measures against the lowest limit among the platforms you selected (or, before you select any, among every platform connected to the profile), and lists each platform whose text will be cut and to how many characters. It informs; the post goes through either way. The composer also lets you write one text per platform instead of one shared caption (“Riêng từng nền tảng”). Each selected platform then gets its own box and is sent that text. The Hub does not cut it, with one exception: on Pinterest posts, YouTube posts and TikTok photo posts, a title box left empty is filled with a title cut from that platform’s own text. A text over that platform’s hard cap is refused naming the box before anything is sent, and an empty box is refused too. Mastodon’s box is sent as typed, as mastodon_caption is.
  • Facebook, X, Threads and Bluesky are never cut. X, Threads and Bluesky auto-split longer text into a thread, Facebook’s limit is 63,206, and Telegram and Discord truncate on their side.
  • Per-platform overrides are validated, for the platforms with a hard cap: Instagram, LinkedIn, TikTok, YouTube, Reddit, Pinterest and Google Business. A <platform>_title (or <platform>_caption) over the cap — e.g. tiktok_title > 90 on a photo post, reddit_title > 300, or instagram_caption > 2,200 — is rejected with a clear error before publishing. X, Threads and Bluesky are not pre-checked because they auto-thread rather than reject, Telegram and Discord truncate, and a Facebook override is only capped on Reels — those go to the platform and report their own result.
  • Links on X. Any URL X would turn into a clickable link is removed from X posts before publishing — put links in your profile or inside the image/video instead.

Advanced platform options

Any option a platform supports can be passed through — MADIAD Hub forwards it untouched, so you’re never limited to the fields listed here. Prefix the field with the platform name where the option is platform-specific. Common examples: Array fields use the field[] convention — repeat the key once per value. For a full reference of every per-platform option — privacy levels, video types, polls, CTAs, subtitle upload, and more — see Platform options.

First comment

Auto-post a follow-up comment the instant a post publishes — handy for a link, hashtags, or a call to action you’d rather keep out of the main caption. Set first_comment as the shared default and override it per platform with <platform>_first_comment (the per-platform value wins where both are present).
Supported on Instagram, Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn. A few platform-specific behaviors:
  • X and Threads — posted as a reply to the post. For an auto-threaded X post, it replies to the last tweet in the thread.
  • YouTube — posted as a top-level comment on the video.
  • Instagram — applies to photo and video posts only (Instagram has no text-only posts).
To attach images to the comment, send first_comment_media[] as binary file uploads, exactly like photos[]. Currently supported on Reddit only, and not available for scheduled or queued posts.

AI-generated captions

Let MADIAD Hub write the caption for you instead of supplying one. Most useful on photo and video posts:

Platform requirements

Facebook posts go to a Page, not a personal profile. If the profile has exactly one Page it is used automatically; if it has several, pass facebook_page_id to choose one. To list a profile’s Pages and find their IDs, use GET /v1/connections/facebook/pages — see Discover Page & board IDs. If a Page is pinned to the profile, it wins over facebook_page_id — see Pin a Page to a profile.
Send photos as binary file uploads (photos[]=@file) or as public links (photo_urls[]) — see Send photos by URL. For video, pass a public video_url.
Reddit posts always carry a title. By default the caption is used as the title; pass reddit_title to set a separate one.

Read the response

A publish call returns the post status, the platforms it targeted, and the identifiers you poll with:
  • completed — every platform published the post; results holds the per-platform outcome. On a publish response it means they all answered on the spot.
  • processing — accepted and still being published; the normal first answer for every kind of post. Poll with request_id, or subscribe to webhooks.
  • scheduled — queued for a future time; poll with job_id. A job_id is set only for a post sent for later: it is the id that cancels or moves the post.
  • draft — no platform refused the post, but at least one took it into the account’s drafts instead of publishing it, so it is not live there. Most often this is TikTok: when the account reaches TikTok’s daily limit on direct posts, the post lands in the TikTok app’s inbox and waits for the account owner to publish it. That platform’s entry in results carries "draft": true, success: true and no url. It is final: do not retry, a retry only adds a second draft.
  • partial — some platforms published and at least one was rejected; check each entry in results.
  • failed — every targeted platform rejected the post. The HTTP status is still 200; the per-platform reasons are in results.
  • canceled — a scheduled post you cancelled before it ran. Nothing was published, results is null, and the status does not change again. A publish call never answers this; a status poll and the post history do.
  • unknown — the outcome could not be confirmed in time (returned with HTTP 502 and code upstream_timeout). The post may already be live — do not retry blindly. The answer carries a request_id: a status poll with it reads unknown until the platforms’ side has picked the post up, then processing and the outcome. See Errors and Idempotency.
Status polls (GET /v1/posts/status) use the same vocabulary.
Expect processing on the first response, for text and photo posts as well as video. A publish call normally answers processing with a request_id, and the outcome comes from a status poll or a webhook. Two answers are already final in the first response:
  • failed — every platform you asked for refused the post. Nothing was published, the upload is not counted, and the same Idempotency-Key can be sent again once the content is fixed. The response can still carry a request_id.
  • scheduled — the post was accepted for a later time (scheduled_at or add_to_queue). Nothing in that response is a verdict yet; the response normally carries both a job_id and a request_id.
When results is present it is keyed by platform. Each entry has success, plus publish_id and url when the post went out, or error and error_code when the platform refused it. An entry with "draft": true was accepted into the account’s drafts and has no url (see draft above):
error is the platform’s own sentence. Where MADIAD Hub has a Vietnamese rendering of it, that follows after · Tạm dịch:, on the publish response and on a status poll alike — the same form as error.message, see Errors. error_code is a short machine-readable reason (for example account_restricted, daily_cap_reached, media_invalid_format) — branch on it rather than on the wording of error. It is present only when the platform gave one. The publish response and a status poll return this identical shape, with one exception: truncated (what was cut to fit, see Character limits) is only on the publish response. When some — but not all — platforms fail, the final status is partial. On the publish response a failed_platforms array lists the platforms reported as failed so far, usually beside processing; a status poll then reports partial, with each refusal in results.
A post can partially succeed — one platform publishes while another fails. Inspect results per platform rather than the top-level status alone.

Warnings: published, but not exactly as asked

A platform can take a post and still leave out part of what you sent — for example a TikTok photo post that names a track with tiktok_music_id on a profile whose TikTok connection has no music permission goes out (or is saved as a draft, with post_mode: MEDIA_UPLOAD) without the music. The status stays what it would be without the warning, so a warnings array is the only place this shows up:
  • warnings is present only when there is at least one; a post without warnings has no warnings key at all.
  • code is music_not_applied (TikTok only: reconnect TikTok on that profile), first_comment_skipped (first_comment was not posted, for example because the post was saved as a draft) or platform_warning (anything else: read message). New codes may be added, so treat a code you do not know as platform_warning.
  • platform is null when a call to several platforms returned the warning without naming one.
  • A warning never means the post failed: do not republish because of one.
  • On a post answered processing, warnings arrive with the status poll, not with the first publish response. A warning returned only with the first answer of a post is on the publish response only, so keep it from there.

Video: answer at once, publish in the background

A video can take the platforms a minute or more. By default the publish call waits for them for a while, and a slow one ends as 502 upstream_timeout (unknown). Send "async_upload": true on POST /v1/posts/video and the call answers within seconds instead: always processing with a request_id, never with per-platform results. The outcome then comes from a status poll or a webhook. A refusal of the request itself (a link that is not a video, a missing YouTube title) is still answered on the spot. request_id is always chosen by MADIAD Hub. One you send in the body is ignored.

Check status later

Poll an asynchronous or scheduled post with the request_id or job_id from the publish response:
For push updates instead of polling, see Webhooks.

Document posts (LinkedIn)

Platforms: LinkedIn only. Formats: PDF, PPT, PPTX, DOC, DOCX.
LinkedIn renders PDF, PPT, PPTX, DOC and DOCX as a swipeable document right in the feed — the only place across the connected channels where a multi-page file is first-class content rather than a link.
The field naming reads oddly once: caption is the DOCUMENT TITLE shown on the card, and description is the commentary above it — both follow how LinkedIn displays the post. caption is required.LinkedIn is the only target — platforms must be exactly ["linkedin"]. Max 100 MB, 300 pages.

Retry the platforms that failed

Platforms: all of them. Retry re-sends only the channels that failed on that post.
When a post reached some channels and not others, retry re-sends only the ones that failed. The file is not uploaded again and no second upload is charged — the quota was taken when the post was first accepted, and a delivery we failed to make is not a new one.
Pass exactly one of request_id or job_id. The response carries a legs array — a post split by a per-platform caption is sent as two jobs, and both are retried. A leg that fully succeeded answers “nothing to retry”; that is the normal answer, not a failure. Do not re-send a partly failed post by calling the publish endpoint again with the same platforms: that is a new post, and it goes live a second time on every platform that already published it. In the dashboard, the Gửi lại kênh lỗi button beside a post on Tổng quan does the same as this call; that page lists only your most recent activity.

Unpublish a live post

post_id is the platform’s id, not our request_id — which is what lets this remove a post you published by hand, outside the Hub. It is the publish_id in the post’s result. For Google Business that is the full name of the post, accounts/…/locations/…/localPosts/…; a bare number answers 400. The answer carries removal, which says what the Hub knows: Not verified: x, linkedin. Both accept a delete for a post id that does not exist, so a 200 there does not prove a post was removed: open the post to make sure. On the other platforms a post id the platform does not know was refused when we measured it (400 on Facebook, 404 not_found on YouTube, Pinterest and Google Business). confirmed is the platform’s own report. The Hub does not read the post back. A Google Business post is removed only when it sits on one of the profile’s own locations; any other location answers 404 not_found.
Unpublishing is irreversible on the platform. We cannot put the post back.Supported: facebook, youtube, x, linkedin, pinterest, google_business. Instagram and TikTok publish no delete endpoint, and a Threads post cannot be removed through the Hub either — a post there can only be removed in the app. All three answer 400 here immediately.