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 syndication

Your 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

MethodPOST https://auth.viafoura.co/authorize_client
AuthorizationHTTP 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>
ReturnsJSON containing access_token (short-lived — used only for the next call)

2. Fetch your site-group signing key

MethodGET https://tyrion.viafoura.co/v3/settings/<your site-group UUID>/keys/customer_container_keys
AuthorizationBearer <access_token> from step 1
ReturnsJSON 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 list

The 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-bit

A 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 PEM

The private_key is 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#8 PRIVATE KEY header, not RSA PRIVATE KEY), or decode and convert with the commands below. If the value already includes the BEGIN/END lines, use it as-is.

echo "PRIVATE_KEY_B64" | base64 -d > key.der
openssl pkey -inform DER -in key.der -out key.pem

You 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_uuid you 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).

AlgorithmRS512
Header{ "alg": "RS512", "typ": "JWT" }
Payload{ "canonical_site_uuid": "<origin site UUID>", "container_id": "<your article id>" } — exactly these two claims, no expiry
Keyyour 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 claims

Stick to canonical_site_uuid and container_id. Some libraries add timestamp claims (iat, exp) automatically — turn that off (e.g. noTimestamp: true in Node's jsonwebtoken). This isn't a validation issue: Viafoura's verifier ignores expiry and extra claims, so an added iat/exp still 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_oubeGJjzpJxw

Embedded 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 only

This 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 your canonical_site_uuid / container_id. Your real token has the same structure with a different signature segment.

📘

No JWT library? A JWT is just base64url(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

Rule: canonical_site_uuid = the site the comment thread originates on.

  • Article native to the brand showing itcanonical_site_uuid = that brand's own site UUID.
  • Article syndicated from another brandcanonical_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.)

🚧

Get canonical_site_uuid right

It 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 manage

The 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:

  1. Visit the page that will be supplying the syndicated content.
  2. Log in with administrator permissions.
  3. In the Viafoura widget open the settings menu and click the "syndicate" button.
  4. Copy the HTML code snippet generated by the widget.
  5. 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 integrations

This 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:

  1. Use your ClientID and ClientSecret to authenticate with Viafoura's API access: Authorization API Endpoint
  2. 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
  3. Save the API response in your database.
  4. 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>
🚧

ContainerID

Note 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();