CORS error loading Genesys Messenger in Next.js

Hey everyone,

Trying to drop the Genesys Cloud web messenger into our internal WFM dashboard built with Next.js. I’ve got the script tag in the _document.js file just like the docs say. The script loads fine on the first render, but when I navigate to another page inside the app, the widget disappears and the console throws a CORS error.

Here’s the error I’m seeing:

Access to script ‘https://engage-usw2.pure.cloud/engage/js/external-api.js’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I’m not making any direct API calls in the frontend, just relying on the SDK. The `` config object looks right:

window..messenger.init({
 organizationId: "my-org-id",
 deploymentId: "my-deployment-id"
});

The widget works fine if I put it in a plain HTML file served via python -m http.server. It seems Next.js’s server-side rendering or hydration is messing with how the external script is fetched or cached. Has anyone run into this with Next.js? I don’t want to disable SSR for the whole app just for this widget. Maybe I need to use next/dynamic or a specific meta tag? The error keeps popping up on every navigation after the initial load. It’s pretty annoying for the team trying to test the new routing logic.

Running into this exact issue on a few internal portals recently. The core problem isn’t actually a CORS misconfiguration on the Genesys side, but rather how Next.js handles hydration and script re-execution during client-side navigation. When you navigate away from the initial page, the DOM is unmounted. The messenger script tries to re-initialize or communicate with the parent window, and since the context has changed or the global object isn’t fully restored, it throws that access error.

Here’s how to fix it properly without breaking the routing:

  • Stop using _document.js for the widget script. It’s too low-level and doesn’t respect the React lifecycle. Instead, create a custom hook that mounts the script only once on the client side.

  • Use useEffect with a cleanup function. This ensures the script is loaded when the component mounts and prevents duplicate injections when navigating between pages.

import { useEffect } from 'react';

export const useGenesysMessenger = (config) => {
 useEffect(() => {
 // Check if script is already loaded to prevent duplicates
 if (window.GenesysWebMessenger) return;

 const script = document.createElement('script');
 script.src = 'https://engage-usw2.pure.cloud/engage/js/external-api.js';
 script.async = true;
 
 script.onload = () => {
 // Initialize only after script is fully loaded
 if (window.GenesysWebMessenger && config) {
 window.GenesysWebMessenger.init(config);
 }
 };

 document.head.appendChild(script);

 // Cleanup: don't remove the script on unmount to preserve state, 
 // but you can add logic here if you need to tear it down completely
 return () => {
 // Optional: window.GenesysWebMessenger?.destroy();
 };
 }, [config]); // Re-run if config changes
};
  • Wrap your layout in a provider. Place this hook in your main _app.js or a top-level layout component. This way, the script persists across page navigations instead of being torn down and rebuilt every time the URL changes.

  • Check your iframe sandbox settings. If you’re embedding this in an iframe for the WFM dashboard, ensure the sandbox attribute allows scripts and same-origin. Sometimes the browser blocks the messenger from writing to the DOM if the sandbox is too restrictive.

It’s usually just the hydration cycle fighting with the global script injection. Moving it to a hook fixes 90% of these cases.