Appearance
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>| Parameter | Required | Description |
|---|---|---|
originUrl | Yes | The origin URL provided to you |
token | Yes | Token used to authenticate with partner backend services. Should allow identifying the user and retrieving data required for initial setup |
target | No | Initial screen to navigate to after authentication. Defaults to pay dashboard. See Navigation Targets |
tenant | Yes | Tenant 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.originagainst the expected CFC origin andevent.source === iframe.contentWindow - Sending: Use the
originUrldomain 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>"
}| Field | Description |
|---|---|
featureName | The feature requiring upgrade ('ar-onboarding' or 'syncedPayments') |
returnUrl | Token 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 Code | Description | Mitigation |
|---|---|---|
EXPIRED_TOKEN | Token expired | Troubleshooting required by the partner |
INVALID_TOKEN | Token already used or invalid | Troubleshooting required by the partner |
MISSING_TOKEN | Token was not provided | Troubleshooting required by the partner |
BAD_RESPONSE | Partner endpoint returned invalid response | Troubleshooting required by the partner |
SERVICE_UNREACHABLE | The service could not be reached | CFC shows error with retry button. Partner can take a recovery path |
TENANT_NOT_FOUND | Tenant could not be found | Contact Fiserv technical services |
UNEXPECTED_ERROR | An unexpected error occurred | Contact 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
- Temporarily replace your CFC iframe URL with the Certification App URL
- The Certification App acts as the iframe, running automated tests to validate your host's postMessage handling
- Once all tests pass, switch back to your production CFC iframe
Steps
- Open the Certification App
- Replace your iframe
srcwith the Certification App URL - Copy the provided code snippet from the app
- Edit the configuration variables (
iframeUrl,iframeID) if needed - Paste the snippet into your browser console
- Wait for the script to connect and load
- Run the tests individually or all at once with "Run All Tests"
What the Tests Cover
| Test | Description |
|---|---|
| App Load | Proper handling of LOADED events, confirming the iframe initializes correctly |
| User Scroll | Correct response to user scroll events |
| Host Resize | Proper adjustment when the host window is resized |
| Content Size Change | Accurate handling of dynamic content height |
| Scroll Lock / Unlock | Correct 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
| Type | Origin | Description | Payload |
|---|---|---|---|
NAVIGATED_TO_TARGET | CFC | Application changed target | target, targetAction |
LOADED | CFC | Page loaded and ready to receive messages | — |
CONTENT_SIZE_CHANGE | CFC | Content size changed | height, width |
SCROLL_TO | CFC | User action requires scroll | relativeScrollX, relativeScrollY, preventScroll |
EXIT_REQUESTED | CFC | User wants to leave CFC | originator |
AUTHENTICATION_ERROR | CFC | Error during authentication | reason, message |
SESSION_EXPIRED | CFC | Session has expired | — |
USER_ACTIVE_PING | CFC | User is still active | — |
SUBSCRIPTION_UPGRADE_REQUEST | CFC | Feature requires upgrade | featureName, returnUrl |
NAVIGATE_REQUEST | Host | Request iframe to change target | target |
USER_SCROLL | Host | User scrolled the host | scrollX, scrollY |
DIMENSIONS_CHANGED | Host | Window resized, loaded, or iframe position changed | windowHeight, windowWidth, elementDistanceFromTop, elementDistanceFromLeft, visualViewportHeight, visualViewportWidth, minimumContentHeight |
SUBSCRIPTION_UPGRADE_FINISHED | Host | Upgrade flow completed | wasUpgraded |