Putting an Agentforce agent on your website: Messaging for Web, the snippet, and the pre-chat trap
You built a Service Agent and it tests beautifully in the preview window — but the chat bubble on your own website is a different deployment entirely. Here's the Messaging for Web build: the channel, the Embedded Service snippet, the domains that 401 you, and the hidden pre-chat field that quietly ships an impersonation hole.
You built an Agentforce Service Agent, it answers cleanly in the Studio preview, and now the obvious ask lands: put it on the website. The homepage, the pricing page, the help center — the chat bubble in the bottom-right corner that every customer expects to find. It feels like the last mile. It is actually its own build, and the reason teams underestimate it is that the preview window is not a channel. It’s a test harness. Getting the agent onto a live page means standing up a Messaging for Web deployment, and that surface has its own object model, its own JavaScript snippet, its own domain rules, and one security footgun that ships an impersonation hole if you wire it the way the quick-starts imply.
This post is the whole website deployment: what Messaging for Web actually is after the 2026 chat retirement, how the Embedded Service snippet gets on the page, the setup that 401s your first load, the pre-chat form, and the hidden-field trap that decides whether your agent can be lied to. We covered the WhatsApp and SMS build separately — the routing mechanics overlap, so this piece stays on the parts that are specific to the browser.
First, the thing that changed under you: legacy Chat is gone
If your muscle memory is “drop in a Live Agent deployment,” retrain it. Salesforce retired legacy Chat (Chat / Embedded Chat, the old Live Agent) on February 14, 2026. It is not a supported target for a new build, and there’s no agent story on it at all. The current product is Messaging for In-App and Web (MIAW) — the web half of which is usually called Messaging for Web or Enhanced Web Chat. Same corner of the screen, completely different plumbing underneath: persistent conversations that survive a page refresh, Omni-Channel routing, and — the reason we’re here — the ability to route to an Agentforce agent.
There is one hard gate before anything else, and it’s the same one that governs messaging on any channel: Agentforce agents run only on Enhanced Messaging, on Enhanced Omni-Channel. A Standard messaging channel supports standard bots, not AI agents. If your org predates the Enhanced stack, the website project quietly becomes a messaging-platform migration first. Check in Setup which one you’re on before you promise a date.
The mental model: three objects and a widget
A website deployment is four moving parts, and it helps to name them before you click, because the Setup UI scatters them:
- A Messaging channel of type Messaging for Web — the inbound endpoint that receives conversations from the browser.
- An Embedded Service deployment — the configuration for the widget itself: branding, position, pre-chat, business hours, and the generated code snippet.
- An Omni-Channel Flow — the router that decides where an incoming conversation goes. This is where the agent gets connected.
- The agent — your Service Agent, sitting behind the flow as a routing target, exactly as it does for WhatsApp.
The one insight that makes the rest click: the agent is not “attached” to the widget. The widget hands the conversation to the channel; the channel launches the Omni-Channel Flow; the flow’s Route Work element points at the agent using the Agentforce Service Agent routing type, with a fallback messaging queue for anything it can’t route. That routing layer is identical to the WhatsApp build, which is why this post doesn’t re-derive it — that piece walks the flow in detail. What’s genuinely different on the web is everything in front of the flow: the snippet, the domains, and the pre-chat form. That’s where the browser-specific mistakes live.
Getting the snippet on the page
Once the channel and deployment exist, the Embedded Service deployment gives you a Get Code action that produces a JavaScript snippet. You paste it into your site — right before the closing </body> tag on a hand-built site, or into the head markup in Experience Builder if you’re deploying to an Experience Cloud site. It looks roughly like this:
<script type="text/javascript">
function initEmbeddedMessaging() {
try {
embeddedservice_bootstrap.settings.language = 'en_US';
embeddedservice_bootstrap.init(
'00Dxx0000001gPz', // your org ID
'Support_Web', // the deployment's API name
'https://mycompany.my.site.com/ESWSupportWeb17000000',
{
scrt2URL: 'https://mycompany.my.salesforce-scrt.com'
}
);
} catch (err) {
console.error('Error loading Embedded Messaging: ', err);
}
}
</script>
<script
type="text/javascript"
src="https://mycompany.my.site.com/ESWSupportWeb17000000/assets/js/bootstrap.min.js"
onload="initEmbeddedMessaging()">
</script>
Two details in there cause most of the “it just doesn’t appear” tickets. The scrt2URL is your org’s real-time messaging endpoint — a separate host from your normal my.salesforce.com domain — and if it’s wrong or unreachable the widget bootstraps and then silently fails to connect. And the whole thing is wired to load after bootstrap.min.js via the onload handler; drop the snippet somewhere a bundler defers or reorders it and embeddedservice_bootstrap is undefined when init runs. Paste it as-is first, confirm the bubble appears, then get clever about where it lives.
The setup that 401s your first load
Here’s the failure that eats an afternoon: everything is configured, the snippet is on the page, and the widget either never renders or throws a CORS/401 in the console. Almost always it’s one of three things, and they’re worth a checklist because none of them are in the happy path of a tutorial:
- The domain isn’t allowlisted. The deployment maintains a list of the domains allowed to embed it. Your production domain, your staging domain, and
localhostif you test locally are all separate entries. A conversation launched from an origin that isn’t on the list gets rejected. This is the single most common cause of a widget that works in the Salesforce preview and dies on your actual site. - The deployment wasn’t published after a change. Edits to the Embedded Service deployment — pre-chat, branding, routing — only reach the live widget when you re-publish. A “why is it still asking for the old fields” bug is nearly always an unpublished change.
- The site isn’t set up for the messaging endpoint. Messaging for Web serves its assets and its live connection from the
my.site.com/salesforce-scrt.comhosts in the snippet. If those URLs are wrong for your org (a common copy-paste error across sandboxes), nothing connects.
None of this is deep. It’s just invisible, and the error messages point at the browser rather than at the config, so budget the debugging time.
Pre-chat: context is good, trusting it is not
Pre-chat is the small form the widget can show before the conversation starts. Used well, it’s how the agent starts a conversation already knowing who it’s talking to instead of burning the first three turns asking. Messaging for Web ships standard fields — the underscore-prefixed _firstName, _lastName, _email, _subject — plus any custom fields you define, and you map them into the Omni-Channel Flow so they arrive as context the agent can read on the very first turn.
But there are two kinds of pre-chat data, and the difference is a security boundary, not a UX preference.
Visible pre-chat is what the customer types into the form. Fine — it’s their claim about themselves, and you treat it as one.
Hidden pre-chat is data your page injects programmatically, without showing the customer, via the setHiddenPrechatFields API. It has to be called after the widget signals it’s ready and before the conversation opens:
window.addEventListener('onEmbeddedMessagingReady', () => {
embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
Origin: 'pricing-page',
Locale: 'en_US',
});
});
This is genuinely useful — it’s how you tell the agent which page the customer launched from, what product they were looking at, what campaign brought them in. It is also client-side code the customer’s browser runs, which means the customer can read it, change it, and send whatever they like. Anyone with dev tools can call setHiddenPrechatFields with any values they want.
A hidden pre-chat field is hidden from the customer’s eyes, not from their control. Every value it carries is an unauthenticated claim from an untrusted browser. Design as if the customer typed it — because effectively, they can.
The trap is putting an identity in there and then trusting it. If your page sets a hidden CustomerId or AccountId and your agent uses that value to look up and act on an account — pull the order history, change an address, read the case list — you have shipped an impersonation hole. A curious customer sets a different id and reads someone else’s data. This is the same pre-chat failure mode we flagged in context and custom variables: never let a client-supplied pre-chat value become authority. If the agent is going to touch account-specific data, the identity has to come from an authenticated source, not a hidden field.
When “who is this?” actually matters: authenticated messaging
For an anonymous marketing bot answering “what’s your return policy,” none of the above bites — there’s no account to impersonate. The moment the agent does anything account-specific, you need authenticated messaging. Messaging for Web supports a verified-identity mode where your already-logged-in web app mints a signed token (a JWT) that Salesforce validates, so the conversation is bound to a real, verified user instead of a self-asserted id. The agent grounds and acts on that identity, and the hidden pre-chat fields go back to being what they’re safe as: routing hints and context, not authorization.
The design rule is clean and worth stating as policy:
- Anonymous deployment → general questions, knowledge answers, lead capture. No agent action reads or writes a specific customer’s records.
- Authenticated deployment → anything that touches an account. The identity comes from the signed token, the agent’s actions are scoped by least privilege, and the server validates every action against the authenticated user rather than the model’s read of a pre-chat field.
Deciding which deployment you’re building is not a late styling choice. It sets your entire trust model, so make it first.
Experience Cloud: the same deployment, a native component
If your “website” is an Experience Cloud site, you don’t hand-paste the snippet. Experience Builder has a native Embedded Messaging component — drop it on the page, point it at the Embedded Service deployment you already created, and it handles the bootstrap for you. The channel, the flow, the agent, and the pre-chat/hidden-field rules are all identical; only the installation of the widget changes. And because an Experience Cloud site already has an authenticated user context for logged-in members, it’s often the cleaner path to the authenticated pattern above — the site knows who the member is, so the agent can too, without you minting tokens by hand.
Where this sits next to the headless option
One honest boundary: Messaging for Web gives you Salesforce’s chat UI, styled to your brand but fundamentally their widget and their conversation model. If you need the agent inside your own React app with your own components, your own message rendering, your own state, that’s not a Messaging for Web job — that’s the Agent API and a headless build, where you drive the agent from your code and own the entire front end. The rule of thumb: reach for Messaging for Web when you want a governed, low-code chat widget on a page fast; reach for the headless Agent API when the agent has to live inside an experience you’ve already built and the standard widget can’t disappear into it. Most teams want the widget. Know which one you’re signing up for before you start, because they’re different projects with different owners.
Takeaways
- The preview window is not a channel. Putting an agent on your site is a Messaging for Web deployment: a channel, an Embedded Service deployment, an Omni-Channel Flow, and the agent behind it as a routing target.
- Legacy Chat retired in February 2026. Build on Enhanced Messaging for Web, and confirm you’re on Enhanced Omni-Channel — agents don’t run on the Standard stack.
- Paste the snippet as-is first. Most “it doesn’t appear” bugs are an un-allowlisted domain, an unpublished deployment change, or a wrong messaging endpoint — not the code.
- Hidden pre-chat fields are client-controlled. They’re hidden from the customer’s eyes, not their hands. Use them for routing context, never as identity the agent trusts to touch an account.
- Account-specific action needs authenticated messaging. Bind the conversation to a signed, verified identity and validate actions server-side; decide anonymous-vs-authenticated before you build anything, because it sets your whole trust model.
- Know when it’s a headless job instead. Messaging for Web is the fast, governed widget; the Agent API is for when the agent must live inside your own app.
The website chat bubble looks like the easy deployment because the agent is already built. It isn’t the agent that’s hard — it’s the seam between an untrusted browser and a system of record, and the pre-chat form is exactly where that seam lives. Get the trust boundary right and the rest is genuinely an afternoon.