Skip to content
ChatfuelSDK
Modules

Publishing

Instagram publishing over four one-way mutations — the three views, the queue the API does not have, and the failure that has already succeeded.

What it is

There is no post entity on the Chatfuel API. The four instagramAccountPublish* mutations take a bot id and some URLs, publish, and answer with an id and a permalink — nothing is stored on the Chatfuel side afterwards. So a draft, a scheduled time, a failure you can retry and a history to look back at are all this app's own, kept in lib/queue/, and the calendar is a view over that queue rather than over the account.

Instagram only, and one account: the one connected to the bot. Before the composer exists at all the module asks whether that account can publish, because a publish into an account without InstagramBusinessContentPublish fails several seconds later, after somebody has written the whole post.

Installed as

Wizard idpublishing
Install itnpx @chatfuel/wizard --modules publishing
Selected byDefault — --yes installs it
RequiresNothing, beyond the implicit core
Recommendsauth, automations
Skillchatfuel-publishing

Neither hidden nor railHidden. auth is recommended for a concrete reason rather than a thematic one: it brings the database a scheduled post has to fire from.

Routes and views

The view is a path segment; everything else is a query parameter. calendar is the default, so it has no segment of its own.

PathWhat it shows
/publishingThe calendar: this app's queue over time, in a month, a week or a list.
/publishing/queueThe same posts as rows, by status, where a failure is read and retried.
/publishing/libraryWhat is already on the Instagram account — posts, carousels, Reels, stories and ads.
ParameterWhat it does
composeThe post whose composer is open: an id, or new.
fromThe InstagramMediaID a new post is being started from, off the library. Only meaningful beside compose=new.
atThe instant a calendar slot handed a new post. Only meaningful beside compose, and cleared with it.
modeCalendar only: month, week (the default) or list.
monthCalendar only, as YYYY-MM. Absent means this one.
statusQueue only: draft, scheduled, publishing, published, failed. Absent means every status.
kindLibrary only: post, reel, story, carousel or ad. Absent means every kind.

An unknown value anywhere falls back in silence, so /publishing/nonsense is the calendar and ?kind=purple is no filter at all. Defaults are left out of what is written, so the back stack does not fill with equivalent addresses.

The library is the only surface here that reads Instagram; the calendar and the queue share a store over the app's own posts. Its refresh action is not a convenience either — the platform serves what it has already pulled down, so an account somebody also posts to from their phone goes stale on its own, and nothing this app publishes appears in the list until a pull has run.

The model

The Chatfuel API publishes immediately. No input takes a time, there is no draft entity, and the only scheduledPublishTime in the whole schema belongs to FbPagePost and is read-only. A content calendar is therefore not a view over something the platform holds. It is a view over a queue the deployment holds, and the queue has to be built.

Which leaves the question a scheduling feature actually turns on: what runs when the time comes? A browser is not an answer — a queue that only fires while somebody has a tab open is a reminder. So there are exactly two shapes, and which one a deployment has decides what the composer may offer:

Where the queue livescanScheduleWhat the composer offers
LocalsetUserStorageItem, the only persistence this API offers a clientfalseSave, and publish now. No time control — absent, not disabled with a sentence beside it.
DatabaseA table on the deployment's own Postgres, with a job on a timertrueA time, and everything that follows from one.

Both implement the same QueueBackend interface, and the difference between them is that one flag. The local store is per signed-in user, so a colleague on the same bot sees their own drafts and not these; a time written into it is a note to self. The module asks the proxy for the scheduled half once at startup, and a 404 is a valid answer — it means the deployment was scaffolded without the database.

The durable shape is worth four notes, because a simpler one gets each of them wrong: claim due rows with for update skip locked before publishing (a publish can run for five minutes, so two ticks will overlap); reap a claim older than about ten minutes, or a function killed mid-publish leaves a post that never goes out and never says why; write the outcome back from the route rather than reading it off the HTTP response; and authenticate the timer's callback with a shared secret, because there is no signed-in user on a cron.

A publish whose HTTP call died may well have succeeded. The mutation blocks while the platform works — around ten seconds for a photo, minutes for a Reel — and the connection can be lost at any point in that. Offering a retry then posts it twice, and there is no unpublish and no delete anywhere in this API. Worse, the obvious signal does not work: measured against a live account a successful publish produced no subscription event, and the post was absent from the media connection until instagramAccountRefetchLatestMedias had run. So the module reads the ids on the account before publishing and, after an unexplained failure, refetches and diffs for twenty seconds. A refusal the server actually sent — a caption too long, a carousel of one — is a decision taken before anything was posted, and a retry after one of those is immediately safe.

That is also why Publishing is a real status rather than a spinner, and why a published row offers Remove from list and never Delete: removing takes it out of this queue and leaves Instagram alone, which is the only thing this API can actually do.

Configuration

VariableNotes
CHATFUEL_TOKENSecret. Server-side only.
VITE_CHATFUEL_WORKSPACE_IDThe workspace you picked during the wizard run.
CHATFUEL_API_BASEDefaults to https://panel.chatfuel.com.
PUBLISHING_SECRETSecret, and optional. Needed only for posts that go out unattended — it is what the app and its database use to prove a request came from the other.

Two more matter to a scheduled deployment and are not this module's own: PUBLIC_URL, where the deployment answers from outside, and VERCEL_AUTOMATION_BYPASS_SECRET on a host that puts authentication in front of every URL. All of them, and which side reads each, are on environment variables.

Permissions

ObjectActionRequired for
BotViewReading the connected Instagram account and what it may do.
BotEditPublishing, and reconnecting the account when its permissions are short.

Those are the Chatfuel permissions. The one that decides whether a publish can happen at all is Instagram's: InstagramBusinessContentPublish, on the connected account. The module treats "no account", "an account that cannot publish" and "ready" as three separate screens rather than one empty state.

Limits

A caption is measured in codepoints, not in JavaScript characters. Instagram accepts 2,200 and refuses 2,201 with InstagramPublishCaptionTooLong — found by bisecting a live account. A caption of 2,200 emoji is accepted, while .length reads those same emoji as 4,400 and a client counting that way refuses a post the platform would have taken.

Thirty hashtags is a warning, not a refusal. Instagram counts thirty and quietly ignores the rest, and there is no hashtag field anywhere in this schema.

The publishing rate limit is invisible. Instagram throttles published posts per rolling 24 hours, at media_publish rather than at container creation, and exposes neither the number nor the remaining allowance. It arrives as a failure and can only be reported as one.

Publishing takes URLs, not files. Every publish input carries a link and Instagram's own servers fetch the bytes, so a URL that needs an Authorization header cannot be published however valid it looks in a signed-in browser.

Nothing here reports how a post did. InstagramPost, InstagramReel, InstagramAd and InstagramStory carry no likes, no reach, no impressions — and no timestamp either, which is why media published outside this app cannot be placed on a calendar day at all. There is no delete and no edit: once it is on the account, this API is finished with it. No publish input takes locations, user tags, product tags, collaborators or alt text.

The module's own ceilings:

Caption2,200 codepoints
Hashtags before a warning30
Items in a carousel2 to 10
One publish5 minutes
Double-publish confirmation window20 seconds
Upload polled for a public address2 minutes
Library page24 items
Media pulled from Instagram on a refresh30
Posts kept in the browser-only queue200, oldest published dropped first

A single feed image, already uploaded, took 9.7 seconds end to end on a live account. Nothing in this module is a form submit, and the five-minute budget is the host's ceiling as much as the API's: a serverless function killed at five minutes takes the request with it whether or not Instagram has finished.

On this page