Embeds: allowed sites and feed polling

Embed
Preview of the card
<a href="https://forumulate.com/guides/embeds"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/67c65a47-80ca-41e0-b1f1-77419027a985/social-card.svg?v=3e7ae33eec8b" alt="Embeds: allowed sites and feed polling" width="600" height="315"></a>

Two integrations control what flows between your own website and your forum. The embeddable-hosts allowlist decides which outside sites may embed your comment threads. Feed polling goes the other way: it watches an RSS or Atom feed and turns each new item into a topic. For the embed widgets themselves, see SDKs and integrations. Both features need org.manage_integrations. To put a whole reader page of any type in an iframe, see ?embed=1 below.

Restrict which sites can embed

The allowlist guards the call an embedded comment thread makes to find or create the topic for the page it sits on (POST /public/orgs/{orgId}/spaces/{spaceId}/embed/resolve). By default that call answers any origin. The moment you add a single host, that changes: only listed hosts may embed, and a request from anywhere else (or with no Origin or Referer at all) is refused with a 403. A listed origin gets its own origin back in Access-Control-Allow-Origin instead of *. Remove the last host and the open behaviour returns.

POST /orgs/{orgId}/embeddable-hosts
{
  "host": "blog.acme.com",
  "categoryId": "…",
  "postAuthor": "Acme Blog",
  "allowedPaths": ["/posts/*", "/about"],
  "className": "article-body"
}

Field

Required

Notes

host

yes

A hostname, an origin or a URL. It is reduced to a bare lowercase hostname, so https://BLOG.acme.com:443/x and blog.acme.com are the same entry. Unparseable hosts are a 422.

categoryId

no

Where topics auto-created for this site's pages land. Matched on the host of the page's URL; an unmapped host uses the space default.

postAuthor

no

Byline for those topics, up to 80 characters. Defaults to Comments.

allowedPaths

no

Up to 100 paths, checked against the path of the embedding page's Referer. A path ending in * matches every path that starts with what comes before it (/posts/*); any other entry must equal the path exactly. A missing Referer is denied.

className

no

Up to 200 characters, stored and returned with the host. Nothing in the embed reads it yet.

Adding a host that already exists updates its mapping instead of failing.

Call

What it does

GET /orgs/{orgId}/embeddable-hosts

List the allowlist.

DELETE /orgs/{orgId}/embeddable-hosts/{hostId}

Remove one host.

Turn a feed into topics

A feed rule polls an external RSS or Atom feed and creates one topic per new item in a space, so a company blog or a release feed shows up in the community without anyone copying it over.

POST /orgs/{orgId}/embed-feeds
{
  "spaceId": "…",
  "feedUrl": "https://blog.acme.com/feed.xml",
  "categoryId": "…",
  "author": "Acme Blog",
  "pollIntervalMinutes": 60
}

Field

Required

Notes

spaceId

yes

The space the topics are created in.

feedUrl

yes

An http(s) URL, up to 2048 characters.

categoryId

no

Category for the new topics.

author

no

Byline, up to 80 characters.

pollIntervalMinutes

no

Whole minutes, 5 to 1440. Defaults to 60.

enabled

no

Pause a feed without deleting it.

Every 10 minutes the platform polls each enabled feed whose interval has passed. Each topic remembers the item's URL, so polling the same item twice never makes a duplicate. A single poll imports at most 20 items, which bounds the backlog when a feed is first added. Adding a URL that is already a feed updates that feed.

Call

What it does

GET /orgs/{orgId}/embed-feeds

List feeds.

PATCH /orgs/{orgId}/embed-feeds/{feedId}

Change space, category, author, interval or enabled.

DELETE /orgs/{orgId}/embed-feeds/{feedId}

Remove it. Topics already created stay.

POST /orgs/{orgId}/embed-feeds/{feedId}/poll

Poll now, ignoring the schedule. Returns { "imported": n }.

Run the poll call right after adding a feed to confirm it imports, rather than waiting for the next scheduled pass.

The widget scripts

The widgets themselves are plain scripts the API serves, which you drop onto your own site with a <script> tag. They are public, need no token, and are cached for an hour:

  • GET /embed/widget.js is the docs search box (ask and search).

  • GET /embed/comments.js is a forum comment thread for the page it sits on.

  • GET /embed/forum.js is a forum's topic list (latest, new, hot or top; a category or a tag).

  • GET /embed/changelog.js is the changelog "What's new" bell and panel.

Each one reads its organization and space from data-org and data-space on the tag; SDKs and integrations lists the other data- attributes. GET /embed/check?org=…&space=… echoes the two ids back as { "ok": true, "org": "…", "space": "…" }, with ok false when either is missing. It does not look them up, so it confirms what your page sends, not that the space exists.

Any reader page in an iframe: ?embed=1

Add ?embed=1 to the address of any reader page — a docs page, a roadmap, a status board, an event, a legal document, the contact form, a careers list, on every type that has reader pages (all but CONTENT, which is delivered by key and has none) — and it renders as a bare frame you can put in an <iframe> on your own site:

<iframe src="https://acme.com/status?embed=1&scheme=dark"
        title="Acme status" style="width:100%;height:480px;border:0"></iframe>

What the frame keeps and what it drops:

Kept

Dropped

The page itself, and the space's theme: colours, fonts, customCss, elementStyles

The header, footer, announcement bar and sign-in

The space's colour scheme, or the one ?scheme= asks for (light, dark or system)

Analytics tags, chat widgets, the consent banner and your customJs

The space's access rules: a space only some networks may read still refuses the rest, and an address that is not a page is still a 404

Scroll motion, the skip link and ads

?scheme= exists because the frame has to match the page around it: a changelog you keep light for your own readers can be embedded dark in a dark product without repainting the whole space. An unrecognised value keeps the space's own choice.

Why there are no analytics or consent banner inside a frame. An embed is a fragment of a page that someone else owns, with that site's consent banner. A consent banner cannot work in a card-sized frame, so a tag in the frame would either fire before anybody was asked or never fire at all, and a framed view counted as a page view would double-count the host page's own. So the frame stays inert, and the page that embeds it measures it.

A page that is embedded is framable from any site (its security policy names no frame-ancestors restriction); the same address without ?embed=1 keeps the reader's normal policy. Inside the frame the page root carries data-reader-embed, so customCss can style the embedded copy differently (see Theming hooks).

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Embeddable Host

Operation

Request

What it does

Shortcuts

EmbeddableHost_list

GET /orgs/{orgId}/embeddable-hosts

List embeddable hosts in an organization

client.embeds.hosts.list()

EmbeddableHost_add

POST /orgs/{orgId}/embeddable-hosts

Create an embeddable host in an organization

client.embeds.hosts.add()

EmbeddableHost_remove

DELETE /orgs/{orgId}/embeddable-hosts/{hostId}

Delete the embeddable host

client.embeds.hosts.remove()

Embed Feed

Operation

Request

What it does

Shortcuts

EmbedFeed_list

GET /orgs/{orgId}/embed-feeds

List embed feeds in an organization

client.embeds.feed.list()

EmbedFeed_add

POST /orgs/{orgId}/embed-feeds

Create an embed feed in an organization

client.embeds.feed.add()

EmbedFeed_update

PATCH /orgs/{orgId}/embed-feeds/{feedId}

Update the embed feed

client.embeds.feed.update()

EmbedFeed_remove

DELETE /orgs/{orgId}/embed-feeds/{feedId}

Delete the embed feed

client.embeds.feed.remove()

EmbedFeed_pollNow

POST /orgs/{orgId}/embed-feeds/{feedId}/poll

Create the poll in an organization

client.embeds.feed.pollNow()

The Forumulate teamUpdated