Use this page when your app embeds the builder in an iframe and you need Save and Save as template buttons in your own UI (outside the iframe). All communication uses the browser postMessage API with the BUILDER_BRIDGE protocol.
All builder and documented API routes require a bearer token. Send the JWT in the Authorization header for fetch/XHR; use ?token= on the iframe src when loading the builder page.
| Situation | Token for mpv2_backend |
|---|---|
| Normal login | token.access_token from mpv2_backend_2 login |
| Admin impersonating | impersonated_user_token.access_token from POST /api/v1/users/{id}/impersonate |
| Launch flow | Call POST /api/builder/launchToken with the JWT above; use returned opaque token in /builder/launch?token=… |
fetch(`${BUILDER_URL}/api/builder/launchToken`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ newsletterid: 123 }),
});
On exit impersonate or logout, discard the impersonated token and reload/close the iframe (revoked server-side). API reference: /documentation.
Every message must use this shape:
| Field | Required | Description |
|---|---|---|
channel | Yes | Always "BUILDER_BRIDGE". Ignore messages with any other channel. |
type | Yes | e.g. "READY", "SAVE_STATE", "CHANGE_STATE", "CAMPAIGN_FIELDS", "SET_CAMPAIGN_FIELDS", "SAVE_REQUEST", "SAVE_RESULT", "SAVE_TEMPLATE_REQUEST", "SAVE_TEMPLATE_RESULT". |
payload | Yes | Object (can be {}). Type-specific data. |
requestId | No | Optional. If you send it with a request, the response will echo it so you can match requests to responses. |
In your window.addEventListener('message', ...) handler, always check event.origin against the builder’s origin (e.g. the origin of your iframe’s src) before using event.data. Only process messages where event.data?.channel === "BUILDER_BRIDGE".
The builder has tabs: Builder, Headers, Footers, and Social. Saving a campaign or template is only valid on the Builder tab (the in-builder Save buttons are disabled on the other tabs).
Your parent app must mirror this: disable your external Save and Save as template buttons when the user is not on the Builder tab.
After READY, the iframe sends SAVE_STATE whenever the active tab changes:
{
channel: 'BUILDER_BRIDGE',
type: 'SAVE_STATE',
payload: {
saveEnabled: true, // enable your Save button
saveTemplateEnabled: true // enable your Save as template button
}
}
On Headers, Footers, or Social, both flags are false. Re-enable them when the user returns to Builder.
Do not enable Save buttons on READY alone — wait for SAVE_STATE (sent immediately after READY). Optionally show a hint such as “Switch to Builder tab to save” when saveEnabled is false.
payload.saveEnabled on each SAVE_STATE.requestId if you sent one). Then hide “Saving...” and show success or payload.error.const builderOrigin = new URL(iframeEl.src).origin;
iframeEl.contentWindow.postMessage({
channel: 'BUILDER_BRIDGE',
type: 'SAVE_REQUEST',
payload: {},
requestId: 'save-' + Date.now() // optional, for correlation
}, builderOrigin);
let ready = false;
let saveEnabled = false;
let saveTemplateEnabled = false;
let saving = false;
let savingTemplate = false;
function updateSaveButtons() {
saveBtn.disabled = !ready || saving || !saveEnabled;
saveTemplateBtn.disabled = !ready || savingTemplate || !saveTemplateEnabled;
}
window.addEventListener('message', function (event) {
if (event.origin !== builderOrigin || event.data?.channel !== 'BUILDER_BRIDGE') return;
const { type, payload, requestId } = event.data;
if (type === 'READY') {
ready = true;
updateSaveButtons();
}
if (type === 'SAVE_STATE') {
saveEnabled = !!payload.saveEnabled;
saveTemplateEnabled = !!payload.saveTemplateEnabled;
updateSaveButtons();
}
if (type === 'SAVE_RESULT') {
saving = false;
updateSaveButtons();
if (payload.ok) {
// Success: payload.newsletterId
} else {
// Failure: show payload.error (e.g. "Switch to the Builder tab to save")
}
}
});
Same idea as Save: you send SAVE_TEMPLATE_REQUEST, the iframe runs “Save as template without content” (user may be prompted for template name inside the iframe), then you receive one SAVE_TEMPLATE_RESULT.
iframeEl.contentWindow.postMessage({
channel: 'BUILDER_BRIDGE',
type: 'SAVE_TEMPLATE_REQUEST',
payload: {},
requestId: 'template-' + Date.now()
}, builderOrigin);
if (type === 'SAVE_TEMPLATE_RESULT') {
if (payload.ok) {
// Success: payload.templateName
} else {
// Failure: show payload.error (e.g. "Template name missing or cancelled", "Duplicate template name")
}
}
The template name is entered by the user in a modal inside the iframe. Your app only triggers the action and then handles success/error.
If your parent app has its own campaign name field, keep it in sync with the builder using these message types:
After READY, the iframe sends the current #campaign_name value whenever the user edits it in the builder:
{
channel: 'BUILDER_BRIDGE',
type: 'CAMPAIGN_FIELDS',
payload: { name: 'My campaign name' }
}
When the user edits the campaign name in your parent UI, push the new value to the iframe:
iframeEl.contentWindow.postMessage({
channel: 'BUILDER_BRIDGE',
type: 'SET_CAMPAIGN_FIELDS',
payload: { name: 'My campaign name' }
}, builderOrigin);
No response is sent. Updates from the parent do not echo back as CAMPAIGN_FIELDS.
/builder/launch?token=<token> (JWT or launch token; optional: &parent_origin=<your origin>). Use impersonated_user_token when impersonating.message; filter by channel === "BUILDER_BRIDGE" and event.origin.saveEnabled / saveTemplateEnabled and update button disabled state.saveEnabled): show “Saving...”, send SAVE_REQUEST, then on SAVE_RESULT handle payload.ok / payload.error.saveTemplateEnabled): send SAVE_TEMPLATE_REQUEST, then on SAVE_TEMPLATE_RESULT handle success or payload.error.