Content publishing for your website
Your feeds and briefs become posts written through the router, reviewed in the dashboard and delivered to your site - a signed webhook, WordPress or Ghost - or pulled as RSS; the same tools on your MCP server.
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). |
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
- From a brief -
POST /api/v1/content/posts/writewithtopic, optionalnotes(facts, figures, names the writer stays within) and optionallyurl, 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, theeditorial-writerskill always in a skill slot - and the post'sroutingsays which model wrote it, at what cost, with which companions.status: publishedpublishes at once; the default is a draft. The answer carrieswriter_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. - From your feeds -
POST /api/v1/content/feedswithurl(RSS 2.0 or Atom), an optionalnameandcategory. 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}/fetchpolls one feed now,POST /api/v1/content/refreshall of them. Posts from feeds wait as drafts unless the profile'sauto_publishis on. - Written elsewhere -
POST /api/v1/content/postswithtitle, a Markdownbody, optionalsummary,tags,source_url,image_urlandstatus- 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) 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_FEEDSfeeds (20) and has up toOSR_PLATFORM_PUBLISHING_MAX_DESTINATIONSdestinations (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, apppublishing) against your plan's quota, budgets and wallet, visible on/platform/dashboard/usageand/platform/dashboard/activitywith 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 withOSR_PLATFORM_PUBLISHING_REFRESH_S(0 = only on request).