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 tophotos[] — 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 withphoto_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 stayhttps://. - 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 withphotos[]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.
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
captiontoo 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
truncatedarray lists each platform and text that was cut (text:title,captionordescription), with the caption’slengthand thelimit. 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>_titleor<platform>_captionyou 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_queuethat 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_captionis. - 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, orinstagram_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. Setfirst_comment as the shared default and override it per platform with <platform>_first_comment (the per-platform value wins where both are present).
- 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).
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 publishes to a Page
Facebook publishes to a Page
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.Photos: upload files or pass URLs; video by URL
Photos: upload files or pass URLs; video by URL
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 needs a title
Reddit needs a title
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 poststatus, the platforms it targeted, and the identifiers you poll with:
completed— every platform published the post;resultsholds 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 withrequest_id, or subscribe to webhooks.scheduled— queued for a future time; poll withjob_id. Ajob_idis 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 inresultscarries"draft": true,success: trueand nourl. 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 inresults.failed— every targeted platform rejected the post. The HTTP status is still200; the per-platform reasons are inresults.canceled— a scheduled post you cancelled before it ran. Nothing was published,resultsisnull, 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 HTTP502and codeupstream_timeout). The post may already be live — do not retry blindly. The answer carries arequest_id: a status poll with it readsunknownuntil the platforms’ side has picked the post up, thenprocessingand the outcome. See Errors and Idempotency.
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 sameIdempotency-Keycan be sent again once the content is fixed. The response can still carry arequest_id.scheduled— the post was accepted for a later time (scheduled_atoradd_to_queue). Nothing in that response is a verdict yet; the response normally carries both ajob_idand arequest_id.
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.
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 withtiktok_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:
warningsis present only when there is at least one; a post without warnings has nowarningskey at all.codeismusic_not_applied(TikTok only: reconnect TikTok on that profile),first_comment_skipped(first_commentwas not posted, for example because the post was saved as a draft) orplatform_warning(anything else: readmessage). New codes may be added, so treat a code you do not know asplatform_warning.platformisnullwhen 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 as502 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 therequest_id or job_id from the publish response:
Document posts (LinkedIn)
Platforms: LinkedIn only. Formats: PDF, PPT, PPTX, DOC, DOCX.
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.
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.

