Maileditor Docs
PluginIntegration

Identity signing

Prove to MailEditor which of your users is opening the editor, with an HMAC signature computed on your server.

Templates and images inside the editor are scoped to external_user_id. Anyone who can set that value can open another user's work, so it must come from your server, which signs the id with the Plugin Secret before the page renders the iframe.

The signature

Build this string, using the length of the user id in bytes:

v1|{byte length of external_user_id}:{external_user_id}|{identity_expires}

Sign it with HMAC-SHA256 using the Plugin Secret as the key, hex encoded. Pass the user id, the expiry and the signature to the iframe as external_user_id, identity_expires and identity_signature.

identity_expires is a Unix timestamp in seconds, at most 15 minutes in the future. Five minutes is a sensible default.

Test vector

Plugin Secret8f2b91c0d4e64a7fa3c5b1d7e9028461
external_user_idcustomer-user-4471
identity_expires1756280000
Payload`v1
Signature902ddd4e1a5c5987ce06e0c4dace6a841755a6a185b57d6fbe8631575cffe933

Code

import { createHmac } from 'node:crypto'

const expires = Math.floor(Date.now() / 1000) + 300
const payload = `v1|${Buffer.byteLength(userId)}:${userId}|${expires}`
const signature = createHmac('sha256', pluginSecret).update(payload).digest('hex')
$expires   = time() + 300;
$payload   = sprintf('v1|%d:%s|%d', strlen($userId), $userId, $expires);
$signature = hash_hmac('sha256', $payload, $pluginSecret);
import hashlib, hmac, time

expires = int(time.time()) + 300
payload = f"v1|{len(user_id.encode())}:{user_id}|{expires}"
signature = hmac.new(plugin_secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
require 'openssl'

expires   = Time.now.to_i + 300
payload   = "v1|#{user_id.bytesize}:#{user_id}|#{expires}"
signature = OpenSSL::HMAC.hexdigest('SHA256', plugin_secret, payload)

A complete Next.js route that returns these values to the page is in the quickstart. Full sign-and-embed examples for other stacks are in the Platform guides.

Where to sign

Sign on the request that renders the page containing the iframe, or in an API route the page calls right before mounting it. Do not sign at build time or cache the result.

The Plugin Secret stays in server-side configuration. Never put it in NEXT_PUBLIC_* variables, client bundles or HTML.

Identity modes

ModeIframe URLUse
signedMust carry all three identity parameters.Default. Use it in production.
openexternal_user_id only. The editor trusts what the page says.Prototyping on a page real users cannot reach.
backendThe editor cannot be opened from a browser. Only your server can create sessions, by sending the secret to the session endpoint. The Backend API keeps working.Server-only integrations.

Set the mode under End user identity on the General Information tab. Changing it ends every live editor session for the plugin.

Common mistakes

SymptomCause
identity_signature_invalid for ids with accents or emojiLength counted in characters instead of bytes.
identity_signature_invalid on every requestWrong secret, for example another plugin's or a stray space copied with it.
identity_signature_expired on slow pagesExpiry too short. Use a few minutes, not seconds.
identity_expires_out_of_rangeExpiry more than 15 minutes ahead, or in milliseconds instead of seconds.
identity_signature_requiredPlugin is in signed mode but the URL has no signature.
external_user_id starts with anon:That prefix is reserved and refused.

Full details of the session request and its errors: session endpoint.

On this page