<!-- OpenSmartRoute: Content publishing for your website. https://opensmartroute.ai/docs/PUBLISHING -->
# Content publishing for your website

The platform writes and publishes content for your own website as a service of your workspace: follow the RSS or
Atom feeds you write about, or send a brief, and the posts are written through the router as **your workspace's
own metered requests**, reviewed in the dashboard (or not, if you say so), then **delivered to your website** - a
signed webhook any site or CMS can receive, a WordPress site, a Ghost publication - or **pulled** from the API and
an RSS feed of your own. The same operations are tools on your MCP server, so an agent in your IDE or your
automation can list, write and publish posts. Everything is scoped to the workspace: another workspace never sees
your feeds, posts or destinations.

The platform's own blog and newsletter run on the same engine; this page is about *yours*.

## Where it lives

| Surface | What you get |
|---|---|
| `/platform/dashboard/content` | Write a post from a brief, review and publish posts, follow feeds, add destinations and test them, set the editorial profile, read the delivery log. |
| `GET /api/v1/content` | The service at a glance: profile, post and delivery counts, feeds, destinations, the writer in use, limits, the pull URL. |
| `/api/v1/content/*` | Every operation below, with your API key (`Authorization: Bearer <key>`). |
| `POST /mcp` | The `content_*` tools on your MCP server (see [MCP](https://opensmartroute.ai/docs/MCP.md)). |

Members write, edit and publish; adding, changing or removing a **destination** (it holds a credential of your site)
takes the `admin` role. API keys act as the workspace and pass every role check.

## Posts: three ways in

1. **From a brief** - `POST /api/v1/content/posts/write` with `topic`, optional `notes` (facts, figures, names the
   writer stays within) and optionally `url`, the page the article is about - its readable text is fetched and
   given to the writer next to your notes. The writer routes the request through
   the platform with a plan - persona, skill and model slots, the `editorial-writer` skill always in a skill slot -
   and the post's `routing` says which model wrote it, at what cost, with which companions. `status: published`
   publishes at once; the default is a draft. The answer carries `writer_decision`: when no executable model is
   available, or the request was refused (quota, policy), the post is your brief itself and the decision says why.
2. **From your feeds** - `POST /api/v1/content/feeds` with `url` (RSS 2.0 or Atom), an optional `name` and
   `category`. The scheduler polls every enabled feed on the deployment's cadence (`OSR_PLATFORM_PUBLISHING_REFRESH_S`,
   an hour by default; a newly added feed replays the last seven days), stores each new entry once (deduplicated by
   the entry's id and by the article URL across your feeds) and writes a post for it from the article page's readable
   text, or the feed's excerpt when the page cannot be fetched. `POST /api/v1/content/feeds/{feed_id}/fetch` polls
   one feed now, `POST /api/v1/content/refresh` all of them. Posts from feeds wait as **drafts** unless the
   profile's `auto_publish` is on.
3. **Written elsewhere** - `POST /api/v1/content/posts` with `title`, a Markdown `body`, optional `summary`, `tags`,
   `source_url`, `image_url` and `status` - your own tool, a form on your intranet, an agent.

`GET /api/v1/content/posts` lists them newest first (`status`, `q`, `limit`, `offset`; bodies left out),
`GET /api/v1/content/posts/{post_id}` has the Markdown body, an HTML rendering and the post's deliveries,
`PATCH /api/v1/content/posts/{post_id}` edits (title, summary, body, tags, image, status) and
`DELETE /api/v1/content/posts/{post_id}` removes a post with its delivery log.
`POST /api/v1/content/posts/{post_id}/regenerate` rewrites a feed post with the current writer, keeping its slug
and status.

Every post has a `slug` (unique within the workspace), `summary`, `why` (one sentence on why it matters to your
readers), `highlights`, `tags`, `keywords` and an `seo` block with a title, description and keywords for your page.

## The editorial profile

`GET|PUT /api/v1/content/profile` - what the writer is told about you: the `publication` name, its `audience`,
the `tone`, the `language` it writes in, the `site_url` where the posts live (the RSS links to it) and
`auto_publish`. Empty fields fall back to plain defaults (the workspace name, "readers of the publication's website
who want the facts first", "plain, factual and friendly", English). The writer never invents facts: it only uses
what the source text or your notes say.

## Destinations: getting posts onto your website

`GET|POST /api/v1/content/destinations`, `PATCH|DELETE /api/v1/content/destinations/{destination_id}`. Each has a
`kind`, a `name`, a `url`, `auto_publish` (every post that becomes published is delivered there) and, for the
CMSs, `post_status` - `publish` (default) or `draft`, when you want a last look inside the CMS. Credentials are
stored encrypted (AES-GCM under the deployment secret, bound to your workspace) and never returned; the URL must
be a public endpoint (a self-hosted deployment that runs its CMS on a private network sets
`OSR_PLATFORM_BYOK_ALLOW_PRIVATE=true`, the same switch as for private model endpoints).

| Kind | What the platform sends | What you provide |
|---|---|---|
| `webhook` | `POST` to your URL: JSON with `event: "post.published"`, the `post` (fields above plus `body` in Markdown and `html`), the `workspace` and the `platform`; headers `X-OSR-Event`, `X-OSR-Delivery` and, with a secret, `X-OSR-Signature: t=<unix>,v1=<hmac-sha256(secret, "<t>.<body>")>` - verify it like a Stripe signature. Answer `2xx`; a JSON body with `id` and `url` is recorded as the post's address on your site. | Any URL that accepts JSON: a route of your site, a serverless function, a CMS plugin, an automation platform. Optional `secret`. |
| `wordpress` | `POST <url>/wp-json/wp/v2/posts` with the title, the HTML content, the excerpt, the slug and the status, authenticated as your user. | The site URL, `username` and an **application password** (Users → Profile → Application passwords) as `secret`. |
| `ghost` | `POST <url>/ghost/api/admin/posts/?source=html` with the title, the HTML, the excerpt, the slug, the tags, the lead image and the source as canonical URL, authenticated with a short-lived token made from your key. | The site URL and an **Admin API key** (`<id>:<secret>`, Settings → Integrations → custom integration) as `secret`. |

`POST /api/v1/content/destinations/{destination_id}/test` sends a clearly marked test post now and answers with
the site's verdict (`ok`, HTTP `status`, `detail`, the `external_url` the site gave it); nothing is stored.

The raw HTML a feed, a model or an editor may have put into a Markdown body is dropped before rendering, so only
what Markdown produces reaches your site.

## Publishing and the delivery log

A post becomes public when it is created or edited with `status: published`, or with
`POST /api/v1/content/posts/{post_id}/publish` - which also delivers it *now* to every enabled destination (or the
`destination_ids` you name), waits for the sites' answers and returns each delivery. A status change alone (PATCH,
auto-publish from a feed, a post created as published) queues one delivery per destination set to `auto_publish`
and sends them in the background at once; the scheduler's minute pass picks up anything left over.

`GET /api/v1/content/deliveries` (`post_id`, `limit`) is the log: every attempt with the destination, the site's
HTTP status, a short `detail`, the `external_id` and `external_url` the site assigned, and `next_at` while a retry
is pending. A delivery is `queued` until its time comes, `sending` during an attempt (an attempt a crashed process
left behind is retried after ten minutes), then `sent` or `failed`. A site that did not answer, failed on its side
(5xx) or asked for a pause (429) is retried after 1, 5,
30 and 120 minutes - five attempts in all; a rejected configuration (401, 403, 404, 400) fails at once with the
site's message. `POST /api/v1/content/deliveries/{delivery_id}/retry` tries again on demand. Each destination
carries its last status, last error and how many posts it accepted.

## Pulling instead of pushing

Your site can fetch the posts itself: `GET /api/v1/content/posts?status=published` and
`GET /api/v1/content/posts/{post_id}` with your API key, or `GET /api/v1/content/feed.xml` - RSS 2.0 of your
published posts, each linked under your `site_url` (or its source when none is set), with the summary, the tags,
the lead image and the source. A static-site build step, a feed reader or an automation platform can consume it.

## From an agent: the MCP tools

Your MCP server (`POST /mcp`, [MCP](https://opensmartroute.ai/docs/MCP.md)) carries, next to `route`, `estimate` and the others:

| Tool | Does |
|---|---|
| `content_list_posts` | List posts (filter by `status`, search with `q`). |
| `content_get_post` | One post by id or slug with its body and deliveries. |
| `content_write_post` | Write a post from a brief (`topic`, `notes`, `url`, `status`) through the router - a metered request of the workspace. |
| `content_create_post` | Store a post the agent wrote itself (Markdown body). |
| `content_publish_post` | Publish a post and deliver it to the destinations now; returns each site's answer. |
| `content_add_feed` | Follow a feed. |
| `content_list_destinations` | The websites posts are delivered to, with their last delivery. |

So "write a post about our new opening hours from this page and publish it to the site" is one instruction to an
agent connected to your workspace - and one entry on your usage, activity and savings pages.

## Limits, metering and privacy

- A workspace follows up to `OSR_PLATFORM_PUBLISHING_MAX_FEEDS` feeds (20) and has up to
  `OSR_PLATFORM_PUBLISHING_MAX_DESTINATIONS` destinations (10); one scheduler pass writes at most ten posts per
  workspace, the rest follow on the next passes.
- Every written post is one metered request (`endpoint: content`, app `publishing`) against your plan's quota,
  budgets and wallet, visible on `/platform/dashboard/usage` and `/platform/dashboard/activity` with its routing
  decision; the models learn from the outcome like from any other request. Listing, editing, publishing and
  delivering are free.
- Feed entries, posts, destinations and deliveries are deleted with the workspace.
- The operator switches the service off with `OSR_PLATFORM_PUBLISHING=false` (no routes, no tools, no scheduler) and
  sets the polling cadence with `OSR_PLATFORM_PUBLISHING_REFRESH_S` (0 = only on request).
