PluginPlatform guides
Vanilla JS
A static HTML page and an Express route that signs the user id, with no framework at all.
The page cannot hold the Plugin Secret, so a small Node server signs the id and serves the page. The page fetches the signed values and sets the iframe src.
Before you start
Follow the first three steps of the quickstart: create a plugin, add localhost:3000 to its allowed domains, and copy the Plugin ID and Secret.
Configuration
MAILEDITOR_PLUGIN_ID=9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10
MAILEDITOR_PLUGIN_SECRET=your_plugin_secret
MAILEDITOR_EDITOR_BASE_URL=https://maileditor.net
MAILEDITOR_PLUGIN_VERSION=1.0.0Sign on the server
Run npm init -y, npm install express, and add "type": "module" to package.json.
import express from 'express'
import { createHmac } from 'node:crypto'
const app = express()
app.use(express.static('public'))
app.get('/api/auth', (req, res) => {
const user = req.query.user
if (typeof user !== 'string' || user === '') {
return res.status(400).json({ error: 'user is required' })
}
const expires = Math.floor(Date.now() / 1000) + 300
const payload = `v1|${Buffer.byteLength(user)}:${user}|${expires}`
const signature = createHmac('sha256', process.env.MAILEDITOR_PLUGIN_SECRET)
.update(payload)
.digest('hex')
res.json({
externalUserId: user,
identityExpires: expires,
identitySignature: signature,
pluginId: process.env.MAILEDITOR_PLUGIN_ID,
pluginVersion: process.env.MAILEDITOR_PLUGIN_VERSION,
editorBaseUrl: process.env.MAILEDITOR_EDITOR_BASE_URL,
})
})
app.listen(3000)Start it with node --env-file=.env server.js and open http://localhost:3000. In a real app, take the user id from the caller's session instead of trusting the user query parameter.
Render the iframe
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Email editor</title>
</head>
<body style="margin: 0">
<iframe
id="editor"
title="Email editor"
allow="clipboard-write; fullscreen"
style="width: 100%; height: 100vh; border: 0"
></iframe>
<script>
fetch('/api/auth?user=customer-user-4471')
.then((res) => res.json())
.then((auth) => {
const params = new URLSearchParams({
external_user_id: auth.externalUserId,
identity_expires: String(auth.identityExpires),
identity_signature: auth.identitySignature,
plugin_version: auth.pluginVersion,
})
document.getElementById('editor').src =
`${auth.editorBaseUrl}/plugin-editor/v1/${auth.pluginId}?${params}`
})
</script>
</body>
</html>The iframe starts without a src and only loads once the signed values arrive.
Next steps
- Add
template_idto the query string to open one of the user's saved templates. See Embedding the editor. - If the iframe shows "Plugin editor unavailable", read
result.errorin thesessionresponse. See Error codes.

.png)