Publications
Validate and schedule approved variants, confirm reminder posts, and handle publication states and conflicts.
A publication records one approved post variant, its channel and its scheduled time. Use it to track delivery state, cost, reminders, attempts and metrics. Scheduling creates the record; publication completes later, after a person confirms a reminder or delivery settles.
The shape of a publication
Field | What it is |
|---|---|
id | Publication id |
publicationId | The same id again, under the name the settled event uses |
postId, channelId | What is being published, and where |
text | Approved variant text rendered as plain network text |
postType | Variant post type captured at scheduling; null for publications created before targets were supported |
fields | Variant fields captured at scheduling, such as a title or subreddit |
costUsd | What publishing this costs, computed when it was scheduled, or null when unknown |
costBasis | plain, link or free, or null |
scheduledAt | ISO-8601 slot it is due in |
state | pending, publishing, published, failed or cancelled |
attempts | Dispatch attempts made so far |
idempotencyKey | The key that makes a retry the same publication |
externalId, externalUrl | The network’s id and permalink, once known |
reminderSentAt, reminderCount | When the most recent reminder went out, and how many have |
publishedAt | When it was confirmed published |
lastError | The most recent failure, if any |
createdAt, updatedAt | ISO-8601 timestamps |
The post retains the Markdown source. The publication stores the exact rendered text, post type and fields selected from the approved version, so a subsequent change cannot alter the queued content.
States
pending ──▶ publishing ──▶ published
│ │
└──────┬──────┴──▶ failed
└────────▶ cancelledpublished, failed and cancelled are terminal; nothing moves out of them.
For reminders, publishing means a person has been notified and still needs to publish and confirm. Confirmation moves the publication to published.
Validate before you schedule
Call content_publications_validate with a postId and up to 20 entries in channels: [{ channelId }]. It checks each network’s current variant against the channel’s capabilities and publishes nothing. The report includes:
ok— true when nothing blocks scheduling.errorCount— how many error-severity issues there are across every channel. Warnings are not counted; they do not block.firstErrorMessage— the first error in report order, ready to show a person.channels[]— one result per requested channel, in the order given, each with the network, the display name, the Markdown that was checked, therenderedTextthat would actually go out, the issue list, the cost, and the daily ceiling that was checked.
You can validate before approval. Validation reads the current draft; scheduling uses the approved version.
Issue codes
The set is stable and append-only.
Code | Meaning |
|---|---|
text_empty | There is no text to publish |
text_too_long | Longer than the channel’s character limit |
too_many_media | More attachments than the channel accepts |
link_not_supported | The network will not take a link |
multiple_links | More links than LinkedIn will preview (advisory) |
media_missing | An attached asset does not exist |
media_not_ready | An attached asset is not stored yet |
media_not_supported | The network will not take this kind of attachment |
video_required | The post type requires video, but no video is attached |
cost_unknown | The cost cannot be computed, which blocks publishing |
daily_ceiling_exceeded | The channel’s daily ceiling is already spent for that slot |
network_not_targeted | The post has no target for this channel’s network |
post_type_not_supported | The channel does not accept the variant’s post type |
field_required | A required variant field is empty |
field_too_long | A variant field exceeds its maximum length |
Each issue includes severity (error or warning), a readable message and the affected input when applicable. Field issues identify fields.<id>, such as fields.title. YouTube uploads and Shorts, Instagram and Facebook Reels, and TikTok video posts require an attached video.
The ceiling report says where the limit came from: channel (an explicit override), adapter (a live figure from the network) or catalog (the built-in fallback).
Cost
Validation reports the calculated cost before you schedule. Scheduling stores that cost on the publication. Both use the channel descriptor’s current rates, including your overrides, rather than reading the built-in rates directly.
costBasis names the rate that was applied:
plain— the flat per-post rate.link— the higher rate a post carrying a link is charged. The default X rates are $0.20 with a link and $0.015 without one.free— the channel’s rates are both zero.
A post counts as carrying a link when its text contains an explicit http:// or https:// URL, a host beginning www., or a bare host ending in a recognized top-level domain. Handles and hashtags are not links. A new or unrecognized top-level domain can be missed and priced as plain text. Include https:// to make link detection explicit.
Scheduling and publishing
Call content_publications_schedule with the approved post, a channel and scheduledAt. A past time queues it for the next scheduler tick. A post already in scheduled state can be scheduled to additional target channels without another approval.
The publication takes the approved variant’s text, post type and fields. Schedule, publish and validate do not accept replacement text: an extra text field receives 400 over REST or an unrecognized-key error over MCP. Set targets[].text on the post and have it approved before scheduling a different network version.
Scheduling rejects a channel outside the post’s targets (network_not_targeted) or a variant that fails the channel’s type, required-field, field-length or video checks. Validate first to see these issues before queueing.
Only one pending, publishing or published publication is allowed for a post on a channel. A second scheduling request returns 409 already_scheduled with the existing publicationId and scheduledAt. Reschedule that publication to change its time, or cancel it before starting again. Once it is published, write a new post to publish on that channel again. Failed and cancelled publications do not reserve the channel.
Concurrent scheduling requests follow the same rule: one succeeds and the other receives already_scheduled. If needed, list publications filtered by postId to inspect the result. Publications created before Content 0.12.1 were not changed, so older records may show more than one live publication for the same post and channel.
content_publications_publish uses the same queue with the current time. Both calls return a queued record. Wait for content/publication.settled and match its publicationId to the returned id to learn the outcome.
Idempotency
Supply a product-unique idempotencyKey to identify a publishing intent. Without one, Content derives a key from the post, channel and time. Repeating a successful request returns its publication before duplicate-scheduling checks run.
A key whose publication failed or was cancelled is spent. Reusing it returns 409 idempotency_key_spent, including publicationId and scheduledAt, and queues nothing. To try again, use a new key or a different scheduled time.
Reminders and confirmation
At the scheduled time, a reminder channel emits content/publication.reminder-due, notifies a person and moves the publication to publishing. The reminder includes the post type and variant fields above the text. Members control these notifications under the publishing scope.
After publishing on the network, call content_publications_confirm. Supply externalUrl to identify the published post and externalId if available. Confirmation stays available while the publication is open, including when the person published earlier but forgot to confirm.
If the first reminder was missed, call content_publications_remind. Content never repeats a reminder automatically because the person might publish twice. The call requires an existing reminder and a reminder channel; an automatic channel returns 409 not_a_reminder_channel.
Retries and failure
On an auto channel, Content sends the queued publication through its bound connector when the slot is due. Read the publication state or wait for content/publication.settled to learn the result.
Automatic publications allow up to five attempts. The four retry delays are 1 minute, 5 minutes, 15 minutes and 1 hour, without jitter. If all attempts are used, this spans a little over an hour.
Retrying is only ever done when it is known to be safe: the publication carries no network id and owes no reconciliation, or the network has been asked and has answered that nothing was published, or the adapter itself reported that nothing was sent.
When the attempts are spent the publication becomes failed and lastError says why. content/publication.failed carries the same short diagnostic, which never contains a credential.
A paused channel holds its publications for later dispatch. Content rechecks the pause instead of marking those publications failed.
Cancelling and rescheduling
Use content_publications_cancel to stop an unfinished publication. For a reminder channel, cancellation stays available after the reminder until confirmation. The response contains the cancelled record, so no settled event is needed to learn that result.
Use content_publications_reschedule to move the same unfinished publication to another time. It returns to the queue, and a new reminder is sent at the new time. Finished publications (published, failed or cancelled) cannot be cancelled or moved.
An automatic publication in publishing state is being sent and its outcome is unknown. Cancel, reschedule, confirm and remind return 409 publication_in_flight. Read content_publications_get or wait for content/publication.settled before acting. This restriction does not apply to reminder publications in publishing state.
If a publication changes between a call’s read and write, the action is checked against its new state. A finished or in-flight publication returns the corresponding conflict; other changes return 409 publication_changed. Read the record again and retry only if the action still applies.
Listing
content_publications_list returns publications newest slot first. Filter by state, postId, channelId, and the slot window scheduledFrom / scheduledTo; page with limit (1 to 100, default 50) and offset. A window whose start is after its end is rejected.
Tools
Tool | What it does |
|---|---|
content_publications_validate | Dry run against one or more channels |
content_publications_schedule | Queue an approved post for a slot |
content_publications_publish | Queue it for this instant |
content_publications_list | List publications, newest slot first |
content_publications_get | Read one publication |
content_publications_cancel | Cancel one that has not finished |
content_publications_reschedule | Move it to a new slot |
content_publications_confirm | Record that a person published it by hand |
content_publications_remind | Send the reminder again |
Next steps
- Channels: where the limits and the rates come from.
- Metrics: what a published publication reports back.