Maileditor Docs
PluginAPI referenceEmbedded API

Session

Issue and revoke the session token every embedded request carries, and the identity signature that proves who the end user is.

The editor calls these endpoints itself. They are documented so you can read the editor's network traffic, understand every error it can show, and create a session from your server when you need one outside the iframe.

Issue a session token

One endpoint, two modes, decided by what you send:

You sendModeCaller
plugin_id + MailEditor-Origin headerBThe editor, from its iframe on your page. No secret anywhere
plugin_id + plugin_secretAYour server. No Origin header
POST /public/plugin/v1/session
FieldTypeRequiredNotes
plugin_idstringyesThe public Plugin ID
plugin_secretstringmode AServer only. Rejected if an Origin header is present
originstringnoMode A only. Restricts the token to this origin. Must be on the allowlist
external_user_idstringnoYour own user identifier. Templates and images are scoped to it. May not begin with anon:
identity_expiresintegersee belowUnix timestamp after which the signature is refused. Required with identity_signature
identity_signaturestringsee below64 hex characters: HMAC-SHA256 of the payload below, keyed with plugin_secret. Requires external_user_id
plugin_versionstringnoIntegration version, for diagnostics

From the browser (mode B)

The browser sets Origin and the editor sets MailEditor-Origin. The allowlist is matched against MailEditor-Origin.

Whether identity_expires and identity_signature are required depends on the plugin's identity_mode: required when signed (the default), optional when open, and no browser session is possible when backend. A signature is verified whenever it is sent, so you can ship signing and confirm it works before switching the mode to signed.

POST /public/plugin/v1/session
Origin: https://maileditor.net
MailEditor-Origin: https://app.customer.com
{
  "plugin_id": "9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10",
  "external_user_id": "customer-user-4471",
  "identity_expires": 1756280000,
  "identity_signature": "902ddd4e1a5c5987ce06e0c4dace6a841755a6a185b57d6fbe8631575cffe933",
  "plugin_version": "1.0.0"
}

200

{
  "message": "Plugin session issued",
  "result": {
    "token": "mep_Xn7Qv2LpR8sT4wY6bK1mJ9zC3hD5fA0gE2uV8iO4rN6qS1tW",
    "token_type": "Bearer",
    "expires_in": 3600,
    "expires_at": "2026-08-13T10:14:22+00:00",
    "mode": "b",
    "tier": "production",
    "origin": "https://app.customer.com",
    "plugin": {
      "plugin_id": "9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10",
      "name": "Acme Editor",
      "version": "1.0.0"
    }
  }
}
FieldNotes
tokenOpaque. Shown once
expires_inSeconds. Call this endpoint again on expiry; there is no refresh token
modea = secret exchange, b = origin-based
tierproduction when the domain is verified, otherwise development
originThe origin the session is restricted to, or null for a mode A token issued without origin

Signing the identity

Without a signature the browser chooses external_user_id freely. The signature proves the identity without putting the secret in the page.

Sign on your server while rendering the page that hosts the editor:

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

For customer-user-4471 expiring at 1756280000 the payload is v1|18:customer-user-4471|1756280000; signed with the secret 8f2b91c0d4e64a7fa3c5b1d7e9028461 it gives the signature in the request above.

identity_expires may be at most 15 minutes ahead. Count the id's length in bytes, not characters. Snippets for Node, PHP, Python and Ruby are on the identity signing page.

From your server (mode A)

Send no Origin header. Add origin whenever the token will be handed to a browser, so it cannot be used anywhere else.

{
  "plugin_id": "9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10",
  "plugin_secret": "8f2b91c0d4e64a7fa3c5b1d7e9028461",
  "origin": "https://app.customer.com",
  "external_user_id": "customer-user-4471"
}

200 has the same shape with "mode": "a". When origin is omitted, result.origin is null and the token works from any origin. Mode A ignores identity_mode: the secret is the proof.

Errors

422 when a secret was sent from a browser, whether or not it is correct.

{
  "message": "A plugin secret must never be sent from a browser: it would be readable by every visitor to your page. Exchange it for a token on your server, or omit it entirely to use origin-based sessions.",
  "errors": { "plugin_secret": ["A plugin secret must never be sent from a browser: …"] }
}

403 origin_not_allowed for a MailEditor-Origin header or an origin field that is not on the allowlist.

{
  "message": "This origin is not allowed for this plugin",
  "result": { "error": "origin_not_allowed", "origin": "https://staging.customer.com" }
}

403 origin_required when neither a secret nor a usable origin arrived, or MailEditor-Origin is missing.

{
  "message": "A valid Origin header is required",
  "result": { "error": "origin_required", "hint": "Browsers set this automatically. Calling from a server? Send plugin_secret instead." }
}

402 quota_exceeded when external_user_id is new to the plugin and the plan's users allowance is spent. A user who already has saved work is never refused, and an anonymous session takes no seat.

{
  "message": "This plugin has used its users allowance of 25 for this period",
  "result": { "error": "quota_exceeded", "feature": "users", "limit": "25" }
}

401 invalid_plugin for an unknown plugin, an inactive plugin, or a wrong secret.

{ "message": "Invalid plugin credentials", "result": { "error": "invalid_plugin" } }

422 origin_invalid when the origin field is malformed.

401 identity_signature_invalid when the signature does not match: the id was edited, the payload was built differently, or the secret is wrong.

{
  "message": "The identity signature is not valid for this plugin",
  "result": { "error": "identity_signature_invalid", "hint": "Sign external_user_id and identity_expires on your server with the plugin secret." }
}

401 identity_signature_expired when identity_expires is in the past. A few seconds of clock drift are tolerated.

422 identity_expires_out_of_range when identity_expires is more than 15 minutes ahead. The response carries max_lifetime_seconds: 900.

403 identity_signature_required when the plugin's identity_mode is signed and no signature arrived.

{
  "message": "This plugin requires a signed identity on browser sessions",
  "result": { "error": "identity_signature_required", "hint": "Send external_user_id, identity_expires and identity_signature, or set the plugin's identity_mode to open." }
}

403 browser_sessions_disabled when the plugin's identity_mode is backend.

422 when external_user_id begins with the reserved anon: prefix.

429 when rate limited: 10 per minute per plugin and IP, 100 per hour and 1000 per day per plugin. Carries Retry-After.

Revoke a session token

The editor calls this when the iframe is removed. The token stops working immediately.

DELETE /public/plugin/v1/session
Origin: https://maileditor.net
MailEditor-Origin: https://app.customer.com
Authorization: Bearer mep_Xn7Qv2LpR8sT4wY6bK1mJ9zC3hD5fA0gE2uV8iO4rN6qS1tW

200

{ "message": "Plugin session revoked", "result": null }

403 origin_mismatch when the token is used from an origin other than the one the session was opened from. This is checked on every authenticated request. A mode A token issued without origin skips the check.

{
  "message": "Origin does not match this session",
  "result": { "error": "origin_mismatch", "origin": "https://evil.example" }
}

401 invalid_token for an expired, revoked or unknown token.

{ "message": "Invalid or expired plugin session", "result": { "error": "invalid_token" } }

On this page