Skip to content
OpenSmartRoute
Documentation
Hosted platform

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.

Hosted platform 7 min read

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

SurfaceWhat you get
/platform/dashboard/contentWrite 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/contentThe 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 /mcpThe 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

  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).

KindWhat the platform sendsWhat you provide
webhookPOST 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.
wordpressPOST <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.
ghostPOST <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:

ToolDoes
content_list_postsList posts (filter by status, search with q).
content_get_postOne post by id or slug with its body and deliveries.
content_write_postWrite a post from a brief (topic, notes, url, status) through the router - a metered request of the workspace.
content_create_postStore a post the agent wrote itself (Markdown body).
content_publish_postPublish a post and deliver it to the destinations now; returns each site's answer.
content_add_feedFollow a feed.
content_list_destinationsThe 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).