Skip to content

DIY Integration (Without the SDK)

If you prefer not to install an external package, you can integrate CFC directly using the browser's postMessage API. This guide covers everything you need to implement manually.

TIP

The CFC SDK handles all of this automatically. This guide is for partners who want full control over the implementation.

Configuring Allowed Domains

You must provide CFC with all domain origins that might host the iframe. This prevents clickjacking by limiting which domains can embed the CFC web application.

Creating the Iframe

Full Page

html
<iframe id="cfc-iframe" src="<auth url>" height="100%" width="100%" style="display:block;border:0;" allow="clipboard-write" />

Partial

html
<iframe id="cfc-iframe" src="<auth url>" height="<mainSectionHeight>px" width="100%" style="display:block;border:0;" scrolling="no" allow="clipboard-write" />

To calculate height, use a fixed height for the header that wraps the CFC iframe:

javascript
const mainSectionHeight = window.innerHeight - HEADER_HEIGHT;

CFC is designed to use as much screen real estate as possible. A fixed height is used because many browsers behave inconsistently with percentage-based heights, and the height can change dynamically (see Synchronizing Dimensions).

See the Integration Guide for guidance on when to use full page vs. partial mode.

Constructing the Auth URL

The auth URL loads the CFC web application:

https://<originUrl>/cfc/auth?token=<token>&target=<target>&tenant=<tenantId>
ParameterRequiredDescription
originUrlYesThe origin URL provided to you
tokenYesToken used to authenticate with partner backend services. Should allow identifying the user and retrieving data required for initial setup
targetNoInitial screen to navigate to after authentication. Defaults to pay dashboard. See Navigation Targets
tenantYesTenant ID provided to you

Establishing postMessage Communication

The host application and CFC communicate via postMessage.

Message Structure

Every message (in both directions) follows this structure:

json
{
  "type": "<MessageType>",
  "messageId": "<uuid>",
  "...additional payload"
}

Receiving Messages

Listen for messages from the CFC iframe immediately after rendering it:

javascript
function listenToCFCMessages() {
	window.addEventListener("message", (event) => {
		if (event.source === document.getElementById("cfc-iframe").contentWindow) {
			const payload = event.data;
			// Handle messages based on payload.type
		}
	});
}

listenToCFCMessages();

Sending Messages

javascript
function sendMessage(type, payload) {
	const { contentWindow } = document.getElementById("cfc-iframe");
	contentWindow.postMessage(
		{
			type,
			messageId: crypto.randomUUID(),
			...payload,
		},
		"*",
	);
}

When CFC is ready to receive messages, it sends a LOADED message.

Verifying Message Origin and Source

In production, always verify both the origin and source of incoming messages, and use the specific originUrl domain instead of "*" when posting messages. This prevents other windows, tabs, or iframes from the same origin from injecting messages:

  • Receiving: Check event.origin against the expected CFC origin and event.source === iframe.contentWindow
  • Sending: Use the originUrl domain instead of "*" when posting messages

Synchronizing URLs

When CFC navigates to a targetable page, it sends a NAVIGATED_TO_TARGET message. The host application should update its URL to match.

json
{
	"messageId": "uuid",
	"type": "NAVIGATED_TO_TARGET",
	"target": "/view-payments"
}

Implementation:

javascript
if (payload.type === "NAVIGATED_TO_TARGET") {
	history.replaceState(history.state, "", `${partnerBasePath}?target=${payload.target}`);
}

Why replaceState?

CFC uses history.pushState internally when navigating. If the host also uses pushState, the browser history stack would have duplicate entries — the user would need to click back twice to reach the expected previous state.

Synchronizing Frame Dimensions and Position

To provide a smooth single-scroll experience between the host and CFC, the position and dimensions of the frame need to be synchronized via two-way communication.

Messages from CFC (incoming)

CONTENT_SIZE_CHANGE — Update the iframe size:

json
{
	"messageId": "uuid",
	"type": "CONTENT_SIZE_CHANGE",
	"height": 1200,
	"width": 800
}

SCROLL_TO — Scroll the host window:

json
{
	"messageId": "uuid",
	"type": "SCROLL_TO",
	"relativeScrollX": 0,
	"relativeScrollY": 100,
	"preventScroll": false
}

Messages to CFC (outgoing)

DIMENSIONS_CHANGED — Send on LOADED, window resize, and whenever the iframe position changes:

json
{
	"messageId": "uuid",
	"type": "DIMENSIONS_CHANGED",
	"windowHeight": 900,
	"windowWidth": 1440,
	"elementDistanceFromTop": 64,
	"elementDistanceFromLeft": 0,
	"visualViewportHeight": 900,
	"visualViewportWidth": 1440,
	"minimumContentHeight": 836
}

USER_SCROLL — Send on every host scroll event:

json
{
	"messageId": "uuid",
	"type": "USER_SCROLL",
	"scrollX": 0,
	"scrollY": 250
}

Full Implementation

javascript
function getIframeElement() {
	return document.getElementById("cfc-iframe");
}

function updateOnContentChange(payload) {
	const frame = getIframeElement();

	if (payload.height != null) {
		frame.style.height = `${payload.height}px`;
	}

	if (payload.width != null) {
		frame.style.width = `${payload.width}px`;
	}
}

// If you have a fixed header height, set it here. Otherwise, the dynamic
// calculation (iframe top + scrollY) is used as a fallback.
const HEADER_HEIGHT = null; // e.g., 64

function updateOnScrollTo(payload) {
	const frame = getIframeElement();
	const headerHeight = HEADER_HEIGHT ?? frame.getBoundingClientRect().top + window.scrollY;

	if (payload.relativeScrollY != null || payload.relativeScrollX != null) {
		window.scrollTo({
			top: headerHeight + (payload.relativeScrollY || 0),
			left: payload.relativeScrollX || 0,
		});
	}

	if (payload.preventScroll) {
		document.documentElement.style.overflow = "hidden";
	} else {
		document.documentElement.style.overflow = "auto";
	}
}

function sendDimensionsChanged() {
	const elementBounds = getIframeElement().getBoundingClientRect();
	const vp = window.visualViewport;
	const distanceFromTop = elementBounds.top + window.scrollY;
	const minimumContentHeight = window.innerHeight - elementBounds.top;

	sendMessage("DIMENSIONS_CHANGED", {
		windowHeight: window.innerHeight,
		windowWidth: window.innerWidth,
		elementDistanceFromTop: distanceFromTop,
		elementDistanceFromLeft: elementBounds.left + window.scrollX,
		visualViewportHeight: vp ? vp.height : undefined,
		visualViewportWidth: vp ? vp.width : undefined,
		minimumContentHeight: minimumContentHeight > 0 ? minimumContentHeight : undefined,
	});
}

function sendUserScroll() {
	sendMessage("USER_SCROLL", {
		scrollX: window.scrollX,
		scrollY: window.scrollY,
	});
}

function listenToCFCMessages() {
	window.addEventListener("message", (event) => {
		if (event.source === getIframeElement().contentWindow) {
			const payload = event.data;

			if (payload.type === "CONTENT_SIZE_CHANGE") {
				updateOnContentChange(payload);
			} else if (payload.type === "SCROLL_TO") {
				updateOnScrollTo(payload);
			} else if (payload.type === "LOADED") {
				sendDimensionsChanged();
			}
		}
	});
}

function initListeners() {
	window.addEventListener("resize", sendDimensionsChanged);

	if (window.visualViewport) {
		window.visualViewport.addEventListener("resize", sendDimensionsChanged);
	}

	window.addEventListener("scroll", sendUserScroll);
	listenToCFCMessages();
}

initListeners();

Cleanup

When tearing down the integration, remove all listeners and reset any state set by the dimension sync — particularly the scroll lock:

javascript
function cleanup() {
	window.removeEventListener("resize", sendDimensionsChanged);
	window.removeEventListener("scroll", sendUserScroll);

	if (window.visualViewport) {
		window.visualViewport.removeEventListener("resize", sendDimensionsChanged);
	}

	// Reset scroll lock in case an overlay was active
	document.documentElement.style.overflow = "";

	const frame = getIframeElement();
	if (frame && frame.parentNode) {
		frame.parentNode.removeChild(frame);
	}
}

Requesting Navigation

To navigate CFC to a different target from the host application:

javascript
sendMessage("NAVIGATE_REQUEST", {
	target: "/view-payments",
});

WARNING

Do not navigate the host and then notify the iframe — this causes a duplicated history stack and breaks the back button. Always send a NAVIGATE_REQUEST and let CFC handle the navigation internally.

If the user clicks a link with the intent to open a new window (ctrl+click, shift+click, etc.), let the browser handle it normally:

javascript
document.getElementById("mylink").onclick = function (event) {
	if (event.ctrlKey || event.shiftKey || event.metaKey || (event.button && event.button === 1)) {
		return; // Let the browser open a new window
	}

	event.preventDefault();
	sendMessage("NAVIGATE_REQUEST", { target: "/view-payments" });
};

Payment Success Event

Detect when a user successfully schedules a payment by listening for a NAVIGATED_TO_TARGET message with targetAction: 'schedulePaymentSuccess':

javascript
if (payload.type === "NAVIGATED_TO_TARGET" && payload.targetAction === "schedulePaymentSuccess") {
	// Handle payment success (show survey, update UI, track analytics, etc.)
}

Message structure:

json
{
	"type": "NAVIGATED_TO_TARGET",
	"target": "/schedule-payment-success",
	"targetAction": "schedulePaymentSuccess",
	"messageId": "<uuid>"
}

Exit Handling

In a full-page iframe integration, the user may indicate they want to return to the host application:

json
{
	"messageId": "uuid",
	"type": "EXIT_REQUESTED",
	"originator": "<the button the user clicked or their intent>"
}
javascript
if (payload.type === "EXIT_REQUESTED") {
	history.pushState({}, "", "/");
}

Subscription Management

Partners can manage their own subscription upgrade experience by listening to upgrade events from the iframe.

Approaches

  • Modal Overlay: Open a modal on top of the iframe and send the result back
  • Full Redirect: Redirect the user to the partner's subscription management page entirely

Upgrade Request (from CFC)

Sent when a user attempts to access a feature that requires an upgrade:

json
{
	"type": "SUBSCRIPTION_UPGRADE_REQUEST",
	"messageId": "<uuid>",
	"featureName": "ar-onboarding",
	"returnUrl": "<token>"
}
FieldDescription
featureNameThe feature requiring upgrade ('ar-onboarding' or 'syncedPayments')
returnUrlToken to navigate back to the originating route via /callback/<returnUrl> (use for full redirect approach)

Upgrade Finished (to CFC)

For the modal overlay approach, send this message after the upgrade flow completes:

javascript
sendMessage("SUBSCRIPTION_UPGRADE_FINISHED", {
	wasUpgraded: true, // or false if cancelled
});

Session Extension

CFC periodically sends USER_ACTIVE_PING while the user is active. Use this to extend the host session:

javascript
if (payload.type === "USER_ACTIVE_PING") {
	resetSessionTimer();
}

When hosted in an iframe, the browser does not inform the host that the user is interacting with the iframe content. Without handling this message, the host session could expire while the user is still active.

Reusing Tokens

If the same token is used in the auth URL that was last used in the same browser session, CFC will skip the token exchange and load faster.

In such cases, handle AUTHENTICATION_ERROR and SESSION_EXPIRED messages by issuing a new token and reloading the iframe.

Error Handling

Authentication Errors

If an error occurs during initial authentication, CFC shows an error page and sends:

json
{
	"messageId": "uuid",
	"type": "AUTHENTICATION_ERROR",
	"reason": "<reason code>",
	"message": "<error message>"
}
Reason CodeDescriptionMitigation
EXPIRED_TOKENToken expiredTroubleshooting required by the partner
INVALID_TOKENToken already used or invalidTroubleshooting required by the partner
MISSING_TOKENToken was not providedTroubleshooting required by the partner
BAD_RESPONSEPartner endpoint returned invalid responseTroubleshooting required by the partner
SERVICE_UNREACHABLEThe service could not be reachedCFC shows error with retry button. Partner can take a recovery path
TENANT_NOT_FOUNDTenant could not be foundContact Fiserv technical services
UNEXPECTED_ERRORAn unexpected error occurredContact Fiserv technical services

Session Expired

json
{
	"messageId": "uuid",
	"type": "SESSION_EXPIRED"
}

Certification Before Production

Before moving to production, you must complete the CFC Embed Certification process using the Certification App:

https://embed-certification.certification.melioservices.com/

How It Works

  1. Temporarily replace your CFC iframe URL with the Certification App URL
  2. The Certification App acts as the iframe, running automated tests to validate your host's postMessage handling
  3. Once all tests pass, switch back to your production CFC iframe

Steps

  1. Open the Certification App
  2. Replace your iframe src with the Certification App URL
  3. Copy the provided code snippet from the app
  4. Edit the configuration variables (iframeUrl, iframeID) if needed
  5. Paste the snippet into your browser console
  6. Wait for the script to connect and load
  7. Run the tests individually or all at once with "Run All Tests"

What the Tests Cover

TestDescription
App LoadProper handling of LOADED events, confirming the iframe initializes correctly
User ScrollCorrect response to user scroll events
Host ResizeProper adjustment when the host window is resized
Content Size ChangeAccurate handling of dynamic content height
Scroll Lock / UnlockCorrect behavior when locking or unlocking host scrolling

Passing Criteria

  • All tests must pass
  • Your integration must respond with the expected events and payloads
  • No console errors or timeouts during the run

Once all tests pass, proceed to production and notify the CFC team with confirmation (screenshots or logs from the Certification App).

Messages Reference

TypeOriginDescriptionPayload
NAVIGATED_TO_TARGETCFCApplication changed targettarget, targetAction
LOADEDCFCPage loaded and ready to receive messages
CONTENT_SIZE_CHANGECFCContent size changedheight, width
SCROLL_TOCFCUser action requires scrollrelativeScrollX, relativeScrollY, preventScroll
EXIT_REQUESTEDCFCUser wants to leave CFCoriginator
AUTHENTICATION_ERRORCFCError during authenticationreason, message
SESSION_EXPIREDCFCSession has expired
USER_ACTIVE_PINGCFCUser is still active
SUBSCRIPTION_UPGRADE_REQUESTCFCFeature requires upgradefeatureName, returnUrl
NAVIGATE_REQUESTHostRequest iframe to change targettarget
USER_SCROLLHostUser scrolled the hostscrollX, scrollY
DIMENSIONS_CHANGEDHostWindow resized, loaded, or iframe position changedwindowHeight, windowWidth, elementDistanceFromTop, elementDistanceFromLeft, visualViewportHeight, visualViewportWidth, minimumContentHeight
SUBSCRIPTION_UPGRADE_FINISHEDHostUpgrade flow completedwasUpgraded