*Social Media Manual — [index](../MANUAL.md)*

# 7. Publishing

Publishing is asynchronous. `social_publish_now` records the intent and queues the work; the
actual platform calls happen in the background within about a minute, with retries. Your job
after the call is to poll `social_get_publication` and report per-account outcomes.

## `social_publish_now`

Arguments: `post_id`, optional `account_ids[]` to publish to a subset of the post's
destinations. The caller must be the owner of every target account, or an editor holding an
authorization that covers the post on it (chapter 3); otherwise `AUTHORIZATION_REQUIRED`.

Before anything is queued the post is previewed (chapter 6); `CONTENT_INVALID` lists the
problems. A disconnected destination is `ACCOUNT_DISCONNECTED`.

Reply: `publication` with `publication_id`, `status: pending`, `post_id`, `revision` (the
revision that will be sent, pinned now), `trigger`, `requested_by`, and one entry per account
in `results[]`, each `status: pending`, `attempt: 1`.

The publication consumes one use of each authorization it rested on and records the evidence.

## Delivery

A queue job picks the publication up (the queue runs every minute). For each destination:

1. the account's session is opened (X tokens are refreshed first if needed);
2. the provider sends the content under the idempotency key `<publication_id>:<account_id>`;
3. the attempt is updated with the outcome.

### Attempt statuses

| status | meaning |
|---|---|
| `pending` | waiting for the queue, or waiting for `next_attempt_at` after a retryable failure |
| `running` | being sent right now |
| `succeeded` | live; `external_id` and `url` are set |
| `retry` | this attempt failed retryably; a new `pending` attempt follows |
| `failed` | gave up: not retryable, or the attempt limit (5) was reached |
| `unknown` | the platform never answered; the post may or may not be up |
| `deleted` | taken down later through `social_delete_published_post` |

A `pending` retry row carries the previous `error_code` and `error_message` and a
`next_attempt_at`, so a poll explains the wait. Retry spacing is the platform's own
retry-after hint when it gives one, otherwise 1, 5, 15, 60 and 240 minutes.

### Platform failures and how they map

| code | retried? | what happened |
|---|---|---|
| `RATE_LIMITED` | yes, after the platform's hint or 15 minutes | the platform throttled the account |
| `PLATFORM_UNAVAILABLE` | yes | 5xx or no connection |
| `RECONNECT_REQUIRED` | no | the credential is dead; the account's `health` becomes `reconnect_required` and the owner must reconnect |
| `PLATFORM_REJECTED` | no | the platform refused the content or the request (message says why) |
| `CAPABILITY_UNSUPPORTED` | no | should have been caught at preview; the provider refused the shape |
| `PUBLICATION_OUTCOME_UNKNOWN` | reconciled | no reply at all (timeout) |

### The `unknown` outcome and reconciliation

When the platform never answers, the attempt is marked `unknown` and a reconciliation job runs
two minutes later. On a platform that deduplicates by idempotency key the job publishes again
under the same key, which either confirms the earlier post or creates it once. Facebook,
Instagram and X do **not** deduplicate, so there the attempt stays `unknown` with the note
"Reconciliation is not possible on this platform; check the account manually". Tell the user
to look at the account before publishing again, or the post may go up twice.

## Publication status

The publication rolls its attempts up:

| status | meaning |
|---|---|
| `pending` | queued, nothing attempted yet |
| `in_progress` | at least one attempt still open |
| `published` | every destination succeeded |
| `partial` | some succeeded, some failed |
| `failed` | every destination failed |
| `unknown` | nothing open, at least one `unknown` |

The post becomes `published` once something succeeded and no calendar slot of it is still
pending.

## Polling

`social_get_publication` with `publication_id` returns the publication with `results[]`
(latest attempt per account: `status`, `attempt`, `external_id`, `url`, `error_code`,
`error_message`, `next_attempt_at`) and the `evidence` (chapter 3).
`social_list_publications` lists them newest first, optionally for one `post_id`.

Poll with a short wait after publishing (delivery needs the queue's next minute), then at
longer intervals while a result is `pending` with a `next_attempt_at` in the future. Report
the `url` of each succeeded destination to the user; report failures with the message and
what you intend to do about them.

## Publishing on someone else's behalf

An editor publishing under an authorization sees exactly the same flow. The platform calls
are attributed to them and billed to the account owner (chapter 10), and the publication's
evidence names the authorization.
