Embed manifest and the reader's Content-Security-Policy

Embed
Preview of the card
<a href="https://forumulate.com/guides/embed-manifest-and-csp"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/1370d947-7ea0-49f2-b32d-80b265ddbc09/social-card.svg?v=3e7ae33eec8b" alt="Embed manifest and the reader's Content-Security-Policy" width="600" height="315"></a>

Integrating a product with Forumulate? Already built for you lists every contract that already exists, so you do not rebuild one.

/.well-known/forumulate-embed.json

GET https://forumulate.com/.well-known/forumulate-embed.json lists every host a Forumulate embed may touch. Use it to write your own site's CSP. It is served with access-control-allow-origin: * and cache-control: public, max-age=3600, and follows the schema it names, https://forumulate.com/schemas/embed-v1.json (served there; the suite's embed-v1 shape). It is compact JSON, kept well under the 256 KB a manifest reader takes, with an ETag: send If-None-Match and an unchanged copy answers 304. On forumulate.com it is always the platform manifest.

The same document answers, with a 200 and never a redirect, at /.well-known/forumulate-origins.json. A manifest reader looks for <product>-embed.json first and follows no redirects. The suite-named copy at /.well-known/forgedeck-embed.json was retired on 2026-10-25: it answers 410 Gone with the manifest's address in its body and Link header. Any other /.well-known/ path is a plain 404.

Key

What it holds

product, updated

forumulate, and when the inputs last changed (a date-time).

openapi, llms, status, changelog

Our contract, our docs index for agents, our status page and our changelog.

surfaces["website-sdk"].csp

For each directive (script-src, connect-src, img-src, style-src, frame-src, font-src, media-src), the exact origins the SDK and its widgets need. Origins only.

surfaces["website-sdk"].requires

Keywords your page must also allow: {}, none. Since @forumulate/embed 1.3.0 the SDK and the hosted widgets set their styles through the CSSOM (constructed stylesheets and style.cssText), which style-src does not govern, so a policy without 'unsafe-inline' works. On a browser without constructed stylesheets (Safari before 16.4) the SDK's one <style> carries your page's nonce: the element's nonce attribute, <meta name="csp-nonce" content="…">, or your first nonced <script>.

surfaces["website-sdk"].trustedTypes

The Trusted Types policy names the SDK creates: [], it creates none. Explanations are under notes.

surfaces["website-sdk"].storage

What the SDK stores, its consent category, its lifetime (12h, 30d, 1y, session, persistent) and its purpose in every language we ship (en always).

surfaces["website-sdk"].sri

The pinned CDN URL and its integrity hash (sha384-…). pinnedUrl repeats the URL.

surfaces["reader-cookies"]

The reader cookies, the same way. A name with {spaceId} or {orgId} is a prefix.

checkers

The User-Agents our link checker, status monitors, cookie scanner and webhooks send, and our egress addresses, so a WAF can let them through.

changes

Every host change. A change that adds or removes an origin is announced at least 30 days before it takes effect.

On your own domain, the same path returns your variant. It is also available at GET /api/public/embed?org=<id or slug>. It adds:

  • your custom-domain hosts;

  • the storage names with your real ids;

  • the connectors you enabled and their hosts;

  • your CMP's script hosts;

  • surfaces["reader-csp"]: the policy your reader pages actually send — csp (origins), requires ('unsafe-inline', data:, blob:) and trustedTypes (["default"]). The 'unsafe-inline' here is the reader page's own: its theme and per-block styles are server-rendered style attributes, which no nonce or hash can allow. It applies only when you put your own proxy in front of reader pages, never to a page that embeds Forumulate.

When your page's CSP blocks one of our origins, the SDK logs:

[Forumulate] blocked by CSP: add <origin> to <directive> (see /.well-known/forumulate-embed.json)

The reader's own CSP on your domain

Reader pages on a custom domain send a nonce plus strict-dynamic policy with Trusted Types (require-trusted-types-for 'script'). It is built from the same data as the manifest, and from what your spaces actually use:

  • WebSockets: exact hosts only. These are the socket host a chat widget declares, and the published hosts of the few analytics vendors that hold one. There is no blanket wss:.

  • frame-src: only the embed providers your published pages and topics actually embed, plus the origins you declared in theme.frameOrigins.

  • Each source once in every directive.

  • Reporting: violations go to /api/public/csp-reports/<org>, through both report-uri and report-to (with a Reporting-Endpoints header).

To see what was refused, call GET /api/orgs/{org}/csp-reports?days=7. It returns one row per day, directive, blocked origin and page, with a count. It keeps only the blocked origin and the page path, never a query string, and keeps reports for 30 days.

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 SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.

Csp Reports Admin

Operation

Request

What it does

Shortcuts

CspReportsAdmin_list

GET /orgs/{orgId}/csp-reports

Content-Security-Policy violations on this organization’s reader pages

client.cspReportsAdmin.list()

The Forumulate teamUpdated