Vue
A Nuxt server route that signs the user id and a page component that renders the iframe.
Nuxt gives a Vue app its own server, so signing lives in a server route and the page fetches the signed values with useFetch. The Plugin Secret stays in private runtime config.
If you use Vue without Nuxt, keep the page component below, replace useFetch with fetch, and sign with any of the server routes from the other guides, for example the Express route in the React guide.
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
Keys placed directly under runtimeConfig are private and only available on the server.
export default defineNuxtConfig({
runtimeConfig: {
maileditorPluginId: '',
maileditorPluginSecret: '',
maileditorEditorBaseUrl: 'https://maileditor.net',
maileditorPluginVersion: '1.0.0',
},
})Nuxt fills them from environment variables with the NUXT_ prefix:
NUXT_MAILEDITOR_PLUGIN_ID=9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10
NUXT_MAILEDITOR_PLUGIN_SECRET=your_plugin_secretSign on the server
import { createHmac } from 'node:crypto'
export default defineEventHandler((event) => {
const { user } = getQuery(event)
if (typeof user !== 'string' || user === '') {
throw createError({ statusCode: 400, statusMessage: 'user is required' })
}
const config = useRuntimeConfig(event)
const expires = Math.floor(Date.now() / 1000) + 300
const payload = `v1|${Buffer.byteLength(user)}:${user}|${expires}`
const signature = createHmac('sha256', config.maileditorPluginSecret)
.update(payload)
.digest('hex')
return {
externalUserId: user,
identityExpires: expires,
identitySignature: signature,
pluginId: config.maileditorPluginId,
pluginVersion: config.maileditorPluginVersion,
editorBaseUrl: config.maileditorEditorBaseUrl,
}
})In a real app, take the user id from the caller's session instead of trusting the user query parameter.
Render the iframe
<script setup lang="ts">
const { data: auth } = await useFetch('/api/auth', {
query: { user: 'customer-user-4471' },
})
const src = computed(() => {
if (!auth.value) return undefined
const params = new URLSearchParams({
external_user_id: auth.value.externalUserId,
identity_expires: String(auth.value.identityExpires),
identity_signature: auth.value.identitySignature,
plugin_version: auth.value.pluginVersion,
})
return `${auth.value.editorBaseUrl}/plugin-editor/v1/${auth.value.pluginId}?${params}`
})
</script>
<template>
<iframe
v-if="src"
:src="src"
title="Email editor"
allow="clipboard-write; fullscreen"
style="width: 100%; height: 100vh; border: 0"
/>
</template>useFetch runs on the server during the first render and in the browser on client-side navigation, so the signature is fresh either way.
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)