Content Syndication
Viafoura offers content syndication between any sites on the allowlist within the same site group.
Content Syndication
Viafoura lets sibling brands in the same site-group share a single comment thread across their sites — a reader sees and joins the same conversation whether they land on Brand A's article or Brand B's syndicated copy of it.
Syndication works through a container signature: a small token attached to the widget that tells Viafoura which site's thread to surface. There are two ways to produce and attach it:
- Self-signed signatures (recommended) — your platform signs the token itself and embeds it in the widget tag. No per-page-load API calls, no token storage, no Viafoura-side code change. Covered first below.
- Admin UI snippet — for a one-off, no-code syndication of a single article, copy a ready-made tag from the widget settings.
A heavier, legacy per-render signature API also exists and is documented at the end for existing integrations.
Prerequisite — ask your Customer Success Manager to enable syndicationYour site-group needs a syndication signing key generated in Viafoura's backend before any of the methods below will work. Confirm with your Customer Success team that this is in place for your site group.
Syndication is available for the Viafoura Conversations and Live Blog components only. Engagement Starter does not currently support syndication.
Recommended: Self-signed signatures
With this approach your platform fetches your site-group's signing key once, then signs each article into a small, never-expiring signature that you embed directly in the widget tag. Viafoura reads the signature, resolves the request to the canonical site's comment thread, and serves it — there is nothing to call at render time and no Viafoura code change required.
How it works
To tell Viafoura "show Brand A's thread on Brand B's page," the widget tag carries a signature:
<vf-conversations class="viafoura" vf-container-id="ARTICLE_ID" signature="SIGNATURE"></vf-conversations>The signature is a JWT signed with RS512 — an asymmetric algorithm, meaning you sign with your site-group's private key and Viafoura verifies with the matching public key (you never share the private key). Its payload is just:
{ "canonical_site_uuid": "<the site the thread lives on>", "container_id": "<your article id>" }It is signed with your site-group's private signing key. Three facts make this light:
- It never expires and is deterministic. The same inputs always produce the same string, so you can re-sign on every render or sign once and cache — your choice. There is no token to refresh.
- No per-request work against Viafoura. Signing happens in your own backend; nothing is fetched from Viafoura at render time.
- You hold your own key. You fetch your site-group's signing key once using credentials you already have, then sign whenever you build a page.
Step 1 — Fetch your signing key (one time)
From your backend, make two API calls using your existing Client ID / Client Secret, then store the result. You only do this once — repeat only if Viafoura rotates your key.
1. Get an access token
| Method | POST https://auth.viafoura.co/authorize_client |
| Authorization | HTTP Basic — Authorization: Basic base64(ClientID:ClientSecret) (most HTTP clients build this for you when you pass the ID/secret as Basic-auth credentials) |
Body (application/x-www-form-urlencoded) | grant_type=client_credentials and scope=<your site-group UUID> |
| Returns | JSON containing access_token (short-lived — used only for the next call) |
2. Fetch your site-group signing key
| Method | GET https://tyrion.viafoura.co/v3/settings/<your site-group UUID>/keys/customer_container_keys |
| Authorization | Bearer <access_token> from step 1 |
| Returns | JSON array of your site-group's enabled signing keys, each with a private_key field (see key-selection note below) |
Pick a key (next callout) and store its private_key as a server-side secret (secrets manager, env var, or restricted file).
Which key to use — the response is a listThe endpoint returns all enabled keys for your site-group — normally two, because key rotation keeps a 2-key rolling window. Don't blindly take the first one: use a key whose status is
enabled, and if more than one is returned with different sizes, pick the ≥2048-bit one.
Key size — RS512 requires ≥2048-bitA spec-compliant RS512 library (RFC 7518 §3.3) will refuse to sign with a key smaller than 2048-bit. Newly provisioned keys are RSA 4096-bit, but a site-group set up before this was enforced may still hold an older 1024-bit key. If your JWT library rejects your key for RS512, contact Viafoura to rotate your keypair.
Converting the key to PEMThe
private_keyis standard Base64 (a single line, no header) of an unencrypted PKCS#8 DER key. Most JWT libraries want PEM. Either wrap the Base64 between-----BEGIN PRIVATE KEY-----/-----END PRIVATE KEY-----(64 chars per line — use the PKCS#8PRIVATE KEYheader, notRSA PRIVATE KEY), or decode and convert with the commands below. If the value already includes theBEGIN/ENDlines, use it as-is.
echo "PRIVATE_KEY_B64" | base64 -d > key.der
openssl pkey -inform DER -in key.der -out key.pemYou will also need, for each brand:
- your site-group UUID (the scope your client credentials are issued for), and
- each brand's site UUID (the
canonical_site_uuidyou put in signatures).
Viafoura will provide these if you don't already have them.
Step 2 — Sign each article
Build a JWT and sign it with the key from Step 1, using any standard JWT/RS512 library in your stack (for example jsonwebtoken in Node, PyJWT in Python, jjwt in Java, firebase/php-jwt in PHP).
| Algorithm | RS512 |
| Header | { "alg": "RS512", "typ": "JWT" } |
| Payload | { "canonical_site_uuid": "<origin site UUID>", "container_id": "<your article id>" } — exactly these two claims, no expiry |
| Key | your site-group private_key from Step 1 |
Put the resulting token in the widget tag's signature attribute:
<vf-conversations class="viafoura" vf-container-id="ARTICLE_ID" signature="THE_SIGNED_JWT"></vf-conversations>Worked example
Signing this payload:
{ "canonical_site_uuid": "1b9c2e7a-5f3d-4c8b-9a21-0bd9aef04417", "container_id": "news/2026/06/example-article" }The client signs it by passing the payload, the RS512 algorithm, and its private key (from Step 1) to a standard JWT library. For example:
import jwt from 'jsonwebtoken';
const signature = jwt.sign(
{ canonical_site_uuid: '1b9c2e7a-5f3d-4c8b-9a21-0bd9aef04417', container_id: 'news/2026/06/example-article' },
privateKeyPem, // your site-group key, loaded once
{ algorithm: 'RS512', noTimestamp: true } // noTimestamp: don't auto-add an "iat" claim
);import jwt
signature = jwt.encode(
{"canonical_site_uuid": "1b9c2e7a-5f3d-4c8b-9a21-0bd9aef04417", "container_id": "news/2026/06/example-article"},
private_key_pem, # your site-group key, loaded once
algorithm="RS512",
)use Firebase\JWT\JWT;
$signature = JWT::encode(
['canonical_site_uuid' => '1b9c2e7a-5f3d-4c8b-9a21-0bd9aef04417', 'container_id' => 'news/2026/06/example-article'],
$privateKeyPem, // your site-group key, loaded once
'RS512'
);
Keep the payload to the two claimsStick to
canonical_site_uuidandcontainer_id. Some libraries add timestamp claims (iat,exp) automatically — turn that off (e.g.noTimestamp: truein Node'sjsonwebtoken). This isn't a validation issue: Viafoura's verifier ignores expiry and extra claims, so an addediat/expstill validates. The reason to omit them is determinism — a timestamp makes the token change every time you sign, which defeats "sign once and cache" and the deterministic-reuse behaviour described below.
Any of the above produces a token like the following — three Base64URL segments (header.payload.signature) joined by dots:
eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJjYW5vbmljYWxfc2l0ZV91dWlkIjoiMWI5YzJlN2EtNWYzZC00YzhiLTlhMjEtMGJkOWFlZjA0NDE3IiwiY29udGFpbmVyX2lkIjoibmV3cy8yMDI2LzA2L2V4YW1wbGUtYXJ0aWNsZSJ9.QK0njeuDd24C5r_xjRZM64gDGid68B-Y2Z65FH_xZ9Fl8JeqHkudNj1hkpCK94TCLA4plN72LCyV8IZpxLGnjYedP5lMw2mHMUuSXGCagZQOoKKtkh-x45H3vleeyAZbBD9kXryfl883RnWC9zpheJp-glzVoTdb3N8BUNgdESIUC-iQNlAFNp6_TjUgaoRN2_CwsSY15E-Wyk4tL5_5LTYY45p6LB3vP1VBJUR_GTfmKeq5zZukUrQEH8ydwHWmqEGUE_zMuDYDlAEHf60ivkLU56Q0R2Xlt7cwSH4CFNZTu-0ks8G1-OLgfKNlv_YfU5bc7fd8m_oubeGJjzpJxwEmbedded in the widget tag:
<vf-conversations class="viafoura" vf-container-id="news/2026/06/example-article" signature="eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJjYW5vbmljYWxfc2l0ZV91dWlkIjoiMWI5YzJlN2EtNWYzZC00YzhiLTlhMjEtMGJkOWFlZjA0NDE3IiwiY29udGFpbmVyX2lkIjoibmV3cy8yMDI2LzA2L2V4YW1wbGUtYXJ0aWNsZSJ9.QK0njeuDd24C5r_xjRZM64gDGid68B-Y2Z65FH_xZ9Fl8JeqHkudNj1hkpCK94TCLA4plN72LCyV8IZpxLGnjYedP5lMw2mHMUuSXGCagZQOoKKtkh-x45H3vleeyAZbBD9kXryfl883RnWC9zpheJp-glzVoTdb3N8BUNgdESIUC-iQNlAFNp6_TjUgaoRN2_CwsSY15E-Wyk4tL5_5LTYY45p6LB3vP1VBJUR_GTfmKeq5zZukUrQEH8ydwHWmqEGUE_zMuDYDlAEHf60ivkLU56Q0R2Xlt7cwSH4CFNZTu-0ks8G1-OLgfKNlv_YfU5bc7fd8m_oubeGJjzpJxw"></vf-conversations>
Illustrative token onlyThis example is signed with a throwaway key, so it won't validate against any real site-group — it only shows the shape. Decode the first two segments (Base64URL-encoded JSON) to confirm the header is
{"alg":"RS512","typ":"JWT"}and the payload matches yourcanonical_site_uuid/container_id. Your real token has the same structure with a different signature segment.
No JWT library? A JWT is justbase64url(header) . base64url(payload) . base64url(signature), where the signature is an RSA-SHA512 sign of the first two segments (joined by.) using your private key. Any crypto library that does RSA-SHA512 can produce it.
The widget transmits the signature to Viafoura as the X-REQUEST-SIGNATURE request header. With the <vf-conversations> tag this is automatic — you only set the signature attribute. If you integrate at the API level instead, send the signed JWT yourself in the X-REQUEST-SIGNATURE header.
The signature is verified automatically by the comment service when the widget loads — you never call a Viafoura signing endpoint at render time.
What to put in canonical_site_uuid
canonical_site_uuidRule: canonical_site_uuid = the site the comment thread originates on.
- Article native to the brand showing it →
canonical_site_uuid= that brand's own site UUID. - Article syndicated from another brand →
canonical_site_uuid= the origin brand's site UUID.
You do not need to track which articles are shared. You can attach a signature to all articles, as long as the rule above holds. When an article is native to the brand rendering it, the signature points at that brand's own site and behaves identically to having no signature — a harmless no-op. (Verified end-to-end.)
Getcanonical_site_uuidrightIt must be the site the container actually lives on. If you point it at the wrong site — e.g. hard-code one fixed brand for every page — the request resolves to a site where that container doesn't exist and you get a 404 / empty thread on reads (and a 403 if you attempt writes). Keep it equal to the article's origin site and you're fine.
Storing the signature is optional
The signature is deterministic for a given (canonical_site_uuid, container_id), so storage is purely an optimization:
- Re-sign on every render (no storage). Perfectly fine — signing is a fast local operation in your backend with no network call.
- Sign once and cache (a DB field on the article, or an in-memory map keyed by article ID) to avoid even that small cost. On a cache miss, just re-sign — same value every time.
Whichever you choose:
- Load the private key once, not per render. Re-signing per render is cheap; re-fetching the key from Viafoura per render is not — don't do that.
- Sign server-side. Only the resulting signature string should ever reach the browser.
No expiry to manageThe only time you must regenerate is if Viafoura rotates your site-group key (rare, coordinated in advance) — treat "signature stopped validating" as the single re-generate trigger.
Security
- The signature is public by design — it lives in page HTML and travels on every reader's request. It grants no special privileges (it cannot moderate, post as a user, or read anything not already public on the canonical page). Safe to embed, cache, or re-derive on demand.
- The signing key is private — keep it server-side and never expose it in browser/client code. Sign at publish time or in a server step, and output only the signature into the page.
- Store the fetched key like any other secret (secrets manager, env var, restricted file).
Admin UI snippet (no code)
For a quick, one-off syndication with no development work:
- Visit the page that will be supplying the syndicated content.
- Log in with administrator permissions.
- In the Viafoura widget open the settings menu and click the "syndicate" button.
- Copy the HTML code snippet generated by the widget.
- Paste that HTML code snippet on the template of the page that will be receiving the syndicated content. This will add a net new widget on the page, with the syndicated content.

Notes
- If the secure key has not been created, the "syndicate" button will return the error message "Failed to fetch content syndication signature".
- If the sites relaying the syndicated content and receiving it are not in the same site group in Viafoura's system, the syndicated widget will not load.
Legacy: per-render signature API
Prefer the self-signed approach for new integrationsThis older flow works, but it adds per-render API calls, a
container_id→ UUID lookup, and short-lived token management that the self-signed approach removes entirely.
It's possible to automate syndication so that, for example, an option in your CMS lets users syndicate content without opening Viafoura's frontend widget:
- Use your ClientID and ClientSecret to authenticate with Viafoura's API access: Authorization API Endpoint
- Use the Bearer token (expires in 5 minutes) from step 1 to call the endpoint that returns the container signature for that specific widget: Get Container Signature API Endpoint
- Save the API response in your database.
- Use the container signature and the containerID of the content being syndicated to build the code snippet that loads the new widget on the receiving page.
Examples
This is an example of a code snippet that generates an original (non-syndicated) conversations widget:
<div class="viafoura">
<vf-conversations vf-container-id="unique-article-id"></vf-conversations>
</div>
ContainerIDNote that in the example above, container ID is being set directly in the widget. Normally we would recommend setting the container ID in the page meta tags.
Learn more about containerID.If you need to fetch the content container UUID, you can do so using your container id here.
This is an example of a code snippet that generates a conversations widget syndicated from the previous example:
<div class="viafoura">
<vf-conversations vf-container-id="unique-article-id" signature="-site-group-secure-signature"></vf-conversations>
</div>Comment Count
The Viafoura comment count widget will not work with syndicated containers. To get the comment count for a syndicated container please use our comment count API and add the count to your page in your own element.
SDK Syndication Implementation
The syndicationKey passed to the SDKs below is the syndication signature token — the same JWT you generate in Step 2 (Sign each article) of the self-signed flow above. Produce it server-side and pass it to the SDK.
iOS snippet:
let commentsVC = VFPreviewCommentsViewController.new(
containerId: "article-123",
containerType: .conversations,
articleMetadata: articleMetadata,
loginDelegate: self,
settings: settings,
syndicationKey: syndicationKey // here
)
let composerVC = VFNewCommentViewController.new(
newCommentActionType: .create,
containerType: .conversations,
containerId: "article-123",
articleMetadata: articleMetadata,
loginDelegate: self,
settings: settings,
syndicationKey: syndicationKey // here
)Android snippet:
VFPreviewCommentsFragment commentsFragment =
new VFPreviewCommentsFragmentBuilder(
"article-123",
articleMetadata,
settings
)
.containerType(VFCommentsContainerType.conversations)
.syndicationKey(syndicationKey)
.build();
VFNewCommentFragment composerFragment =
new VFNewCommentFragmentBuilder(
VFNewCommentAction.CREATE,
VFCommentsContainerType.conversations,
"article-123",
articleMetadata,
settings
)
.syndicationKey(syndicationKey)
.build();