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 11 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 - and to your social accounts (LinkedIn, X, Facebook, Threads, Discord, Telegram, Mastodon, Bluesky, Reddit, or re-posted on Dev.to, Hashnode, Medium, Blogger, Tumblr with a canonical link), each on its own posting schedule - 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 (check a URL first, rename, recategorise, pause, fetch now), add destinations and test them, copy or replace the RSS pull link, 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, RSS 1.0 or Atom - or the page of a blog: the feed it advertises in its <head>, or at /feed, /feed.xml, /rss.xml, /rss, /atom.xml or /index.xml, is the one followed), an optional name (the feed's own title when left out) and category (a tag on every post from it). The URL is looked at once before it is followed: the answer carries the feed with what was found under probe (ok, title, site_url, entries, the newest recent entries, discovered_from when a page led to the feed); a page that answers but has no feed is refused with the reason (400), a URL that does not answer is followed with the error on its row and tried again on every pass. POST /api/v1/content/feeds/preview is the same look without following - the dashboard's Check button - and also says whether you follow it already. 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; PATCH /api/v1/content/feeds/{feed_id} renames, recategorises or pauses a feed (enabled), DELETE stops following it (its posts stay). Each feed carries its last fetch, its last HTTP status and error, how many entries it has yielded and the site it belongs to. 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), auto_publish and article_words - the length a post aims at (300 to 4,000 words; 0 or empty = the deployment's default, 2,000). 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.

How a post is written

Every post from a feed or a brief is written in two metered passes, both routed through the platform with the editorial-writer skill in a skill slot: first the brief - a small JSON object with the title, the summary, the key points, why it matters, an outline of sections (three to nine, scaled to the length asked), tags and keywords - then the article itself, plain Markdown from that outline, written for your publication, its readers and tone, in your language, from the source page's readable text (a feed entry) or from your notes and the linked page (a brief). A draft that stops short of three quarters of the length asked is continued once from where it ended. The post's routing carries both passes (passes), the measured length (length: words, target, minimum, whether it came in short) and, when the article pass could not produce at least 400 words, a reason - the post then keeps the brief's own text or your notes. GET /api/v1/content reports the lengths in writer.article.

Destinations: getting posts onto your website - and your social accounts

GET|POST /api/v1/content/destinations, PATCH|DELETE /api/v1/content/destinations/{destination_id}. Each has a kind, a name, a url (the site, for the three CMS kinds), auto_publish (every post that becomes published is delivered there), a schedule (below) and, for the CMSs and blog platforms, 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.
linkedin, x, facebook, threads, mastodon, bluesky, redditA message on your account or page - title, summary, the link to the post on your site (the profile's site URL plus the slug; the source article when you have none) and hashtags from the tags, or your own template with {title}, {summary}, {url}, {hashtags}, {brand}, {why} - fitted to the network's limit without cutting the link, with the post attached as a link card where the network has one.credentials: the fields GET /api/v1/content/destinations lists under networks for the kind (a LinkedIn access token and author URN; X's four OAuth 1.0a values or an OAuth 2 user token; a Facebook page id and page token; a Threads user id and token; a Mastodon instance and token; a Bluesky handle and app password; a Reddit script app, account and subreddit). No url.
discord, telegramA channel message with an embed or link preview: title, summary, link, lead image.A Discord webhook URL; a Telegram bot token and chat id.
devto, hashnode, medium, blogger, tumblrThe article itself (Markdown or HTML as the platform wants it, the tags, the lead image) with your post's page as the canonical URL, published or as a draft (post_status).A Dev.to API key; a Hashnode token and publication id; a Medium integration token; a Blogger blog id and OAuth client with refresh token; Tumblr's blog and OAuth 1.0a values.

Schedules. schedule decides when a queued delivery goes out: {"mode": "immediate"} (default) sends the moment a post is published; {"mode": "window", "days": [0,1,2,3,4], "start": "09:00", "end": "17:00", "timezone": "Europe/Berlin"} waits for the next weekday (0 = Monday) and time of day in that zone; gap_minutes keeps two deliveries to the same destination at least that far apart and max_per_day caps them per local day - so a feed that drops five stories at once posts them spread out instead of flooding your followers. The slot is computed after the last delivery sent or planned on that destination. An explicit POST /api/v1/content/posts/{post_id}/publish always goes now.

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 at the time its schedule allows and sends what is due in the background at once; the scheduler's minute pass picks up anything left over and the deliveries whose slot has come.

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 feed reader cannot send an API key, so the feed also answers to the workspace's feed token in the query string: the overview's feed_url is the complete pull URL (.../api/v1/content/feed.xml?token=...), shown with a copy button on the Content page under Destinations. The token reads published posts and nothing else; POST /api/v1/content/feed/token (admins; Replace link on the page) replaces it, and the old link stops working at once. 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_list_feedsThe feeds the workspace follows, each with its last fetch and error.
content_add_feedFollow a feed, or the page of a blog (the feed it advertises is found); the answer says what was found under probe.
content_fetch_feedPoll one feed now and write posts for its new entries (a metered request per post).
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).