Installation
Choose the installation method that fits your project setup.
npm / yarn / pnpm (Recommended)
Best for React, Vue, Angular, Next.js, Nuxt, and any bundler-based project. Gives you full TypeScript support, tree-shaking, and module imports.
# npm
npm install @min8t.com/plugin-sdk
# yarn
yarn add @min8t.com/plugin-sdk
# pnpm
pnpm add @min8t.com/plugin-sdkUsage in your code:
// Import as ES module
import min8t from '@min8t.com/plugin-sdk';
// Or import specific types for TypeScript
import min8t, { PluginConfig, PluginApiResponse } from '@min8t.com/plugin-sdk';
// Initialize
await min8t.init({
pluginId: 'min8t_pk_your_plugin_id',
apiRequestData: { emailId: 'email-123' },
getAuthToken: () => fetch('/api/editor-token').then(r => r.json()).then(d => d.token),
});The npm package includes TypeScript definitions (dist/index.d.ts). No additional @types package needed. Supports ESM and CommonJS imports.
CDN Script Tag
Best for vanilla HTML/JS, WordPress, Shopify, or any page without a build system. Adds `window.min8t` globally with no import needed.
<!-- 1. Add the script tag -->
<script src="https://plugins.min8t.com/min8t.js"></script>
<!-- 2. Create a container element (the SDK mounts the editor here) -->
<div id="min8t-plugin" style="width: 100%; height: 600px;"></div>
<!-- 3. Initialize -->
<script>
window.min8t.init({
pluginId: 'min8t_pk_your_plugin_id',
apiRequestData: { emailId: 'email-123' },
getAuthToken: () => fetch('/api/editor-token').then(r => r.json()).then(d => d.token),
});
</script>The script tag automatically creates window.min8t on load. The SDK API base URL defaults to https://plugins.min8t.com in production and http://localhost:3009 in development. You can override it with the baseUrl config option or a data-min8t-api attribute on the script tag.
Editor Container
Both installation methods require a container element where the editor iframe will be mounted.
<!-- The SDK looks for #min8t-plugin by default -->
<div id="min8t-plugin" style="width: 100%; height: 600px;"></div>
<!-- The container must exist in the DOM before calling init() -->
<!-- The SDK will create the container automatically if not found, -->
<!-- but setting explicit dimensions gives you control over the layout -->The container ID must be "min8t-plugin". The SDK clears the container contents and inserts an iframe. Set width/height on the container to control the editor size. The iframe fills 100% of the container.
Credentials & API Keys
Every project gets two credentials: a **Plugin ID** (public, used in frontend code) and a **Secret Key** (private, used only on your backend server). Both are found in **Developers Hub → Dev Platform → Projects → Credentials** tab.
Plugin ID
min8t_pk_ followed by 48 hex characters (e.g. min8t_pk_a1b2c3d4e5f6...)Secret Key
min8t_sk_ followed by 48 hex characters (e.g. min8t_sk_x9y8z7w6v5u4...)Setting Up Your Secret Key
Store your Secret Key as an environment variable on your backend server. Never commit it to source control.
# .env file (Node.js with dotenv, Python with python-dotenv, etc.)
MIN8T_SECRET_KEY=min8t_sk_your_secret_key_here
# Docker / docker-compose.yml
environment:
- MIN8T_SECRET_KEY=min8t_sk_your_secret_key_here
# Vercel (dashboard → Settings → Environment Variables)
# Name: MIN8T_SECRET_KEY
# Value: min8t_sk_your_secret_key_here
# AWS / Heroku / Railway / Render
# Add MIN8T_SECRET_KEY in your platform's environment variable settings
# Linux/macOS terminal (temporary, for testing only)
export MIN8T_SECRET_KEY=min8t_sk_your_secret_key_hereAccessing the Secret Key in Your Backend Code
// Node.js: reads from .env via dotenv or process.env
const MIN8T_SECRET_KEY = process.env.MIN8T_SECRET_KEY;
# Python: reads from .env via python-dotenv or os.environ
MIN8T_SECRET_KEY = os.environ['MIN8T_SECRET_KEY']
// Go: reads from environment
min8tSecretKey := os.Getenv("MIN8T_SECRET_KEY")
// PHP: reads from environment
$min8tSecretKey = getenv('MIN8T_SECRET_KEY');
# Ruby: reads from environment
MIN8T_SECRET_KEY = ENV['MIN8T_SECRET_KEY']The Quick Start and Authentication sections show complete working examples for each language, including how the Secret Key is used to generate HMAC-SHA256 signed tokens.
Quick Start
Get the MiN8T editor running in your app in 5 minutes.
Install
Install via npm (recommended for React, Vue, Angular) or add the CDN script tag for vanilla JS. See the Installation section above for detailed setup.
# npm (recommended for bundler-based projects)
npm install @min8t.com/plugin-sdk
# Or use the CDN script tag (for vanilla HTML/JS)
<class="code-tag">script src="https://plugins.min8t.com/min8t.js"></class="code-tag">script>
<!-- Create a container for the editor -->
<class="code-tag">div id="min8t-plugin" style="width: 100%; height: 600px;"></class="code-tag">div>Configure
Create a configuration object with your plugin ID, email ID, and auth token provider.
const config = {
pluginId: class="code-string">'YOUR_PLUGIN_ID',
apiRequestData: {
emailId: class="code-string">'email-123',
},
getAuthToken: () => fetchTokenFromYourBackend(),
locale: class="code-string">'en',
theme: class="code-string">'light',
};Authenticate (server-side)
Your backend uses the Secret Key (from Developers Hub → Dev Platform → Projects → Credentials) to generate an HMAC-SHA256 signed token. Store the Secret Key as an environment variable. See the Credentials & API Keys section for setup instructions.
class="code-comment">// 1. Load your Secret Key from environment variable
class="code-comment">// (see Credentials & API Keys section for how to set this up)
const MIN8T_SECRET_KEY = process.env.MIN8T_SECRET_KEY;
class="code-comment">// 2. Your backend endpoint (called by the frontendclass="code-string">'s getAuthToken())
app.post('/get-editor-token', async (req, res) => {
const token = generatePluginToken(req.body.pluginId, req.user.id);
const response = await fetch(class="code-string">'https:class="code-comment">//plugins.min8t.com/init', {
method: class="code-string">'POST',
headers: {
class="code-string">'Content-Type': class="code-string">'application/json',
class="code-string">'ES-PLUGIN-AUTH': token,
},
body: JSON.stringify({
pluginId: req.body.pluginId,
emailId: req.body.emailId,
}),
});
const data = await response.json();
res.json({ token: data.sessionId });
});
class="code-comment">// See the Authentication section below for generatePluginToken() implementationInitialize
Call `init()` to start the editor. The SDK creates an iframe, authenticates, and loads the email template.
class="code-comment">// Initialize the editor
await window.min8t.init(config);
console.log(class="code-string">'Editor is ready!');Use
The editor mounts in the `#min8t-plugin` container. Use the JavaScript API to save, export, or retrieve content.
class="code-comment">// Get current HTML/CSS
const { html, css } = await window.min8t.getHtml();
class="code-comment">// Save to backend
const result = await window.min8t.save();
console.log(class="code-string">'Saved at:', result.savedAt);
class="code-comment">// Export as HTML
const exported = await window.min8t.export(class="code-string">'html');
console.log(class="code-string">'Download:', exported.downloadUrl);
class="code-comment">// Clean up when done
window.min8t.destroy();Authentication
The Plugin SDK uses ES-PLUGIN-AUTH token-based authentication with HMAC-SHA256 signing.
Token Exchange Flow
┌──────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Browser │ │ Customer Backend │ │ MiN8T Plugin │
│ (SDK) │ │ │ │ Service (:3009) │
└─────┬─────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ 1. Request token │ │
│ ──────────────────>│ │
│ │ 2. Generate token: │
│ │ HMAC-SHA256 of JSON │
│ │ payload (hex digest), │
│ │ base64(payload.sig) │
│ │ │
│ │ 3. POST /init │
│ │ ES-PLUGIN-AUTH: token │
│ │ {pluginId, emailId} │
│ │ ───────────────────────>│
│ │ │
│ │ 4. {sessionId, │
│ │ editorUrl} │
│ │ <───────────────────────│
│ 5. Returns editorUrl│ │
│ <──────────────────│ │
│ │ │
│ 6. SDK creates │ │
│ <iframe src= │ │
│ editorUrl> │ │
│ ──────(browser)───────────────────────────> │
│ │ │
│ 7. Editor loads, │ │
│ SDK ↔ Editor │ │
│ via postMessage │ │
Token format: base64( JSON(payload) + "." + hex(HMAC-SHA256(payload)) )
Example decoded: {"pluginId":"abc","userId":1,"expires":1735689600000}.a1b2c3...
Note: Session Context Enrichment:
After authentication, the plugin service resolves companyId and pluginTier for
the authenticated plugin by calling the User Service internal endpoint
(GET /internal/plugin-id/{pluginId}/company). Results are cached in Redis for
5 minutes to reduce cross-service calls. The pluginTier value determines which
rate limiting tier is applied to subsequent API requests (e.g., free vs. pro
vs. enterprise limits).
Note: Allowed Origins & Security:
The SDK validates the origin of all postMessage events between the host page
and the editor iframe against the project's configured allowedOrigins list.
• Wildcard mode (development): Set allowedOrigins to ['*'] to permit all
origins. Suitable for local development only, not recommended for production.
• Production mode: List specific origins (e.g., ['https://app.example.com']).
Only requests from whitelisted origins are accepted; all others are silently
dropped.
• Configuration: Manage allowed origins in Developers Hub → Dev Platform →
Projects → Origins tab. Changes take effect on the next plugin initialization.
• postMessage validation: The SDK checks event.origin on every incoming
message from the editor iframe. If the origin does not match the configured
list, the message is ignored. Developers embedding the editor in complex
iframe hierarchies must ensure their hosting domain is whitelisted.Backend Token Generation
const crypto = require(class="code-string">'crypto');
const express = require(class="code-string">'express');
const app = express();
const MIN8T_SECRET_KEY = process.env.MIN8T_SECRET_KEY; class="code-comment">// Your Secret Key from Developers Hub → Dev Platform → Projects → Credentials
function generatePluginToken(pluginId, userId) {
const payload = {
pluginId,
userId,
expires: Date.now() + 24 * 60 * 60 * 1000, class="code-comment">// 24 hours
};
class="code-comment">// 1. HMAC input is the raw JSON string (NOT base64-encoded)
const payloadStr = JSON.stringify(payload);
class="code-comment">// 2. Signature is hex-encoded (NOT base64)
const signature = crypto
.createHmac(class="code-string">'sha256', MIN8T_SECRET_KEY)
.update(payloadStr)
.digest(class="code-string">'hex');
class="code-comment">// 3. Final token: single base64 wrap of class="code-string">"payload.signature"
return Buffer.from(`${payloadStr}.${signature}`).toString(class="code-string">'base64');
}
class="code-comment">// Token exchange endpoint (called by your frontend)
app.post(class="code-string">'/get-editor-token', async (req, res) => {
const { pluginId, emailId } = req.body;
const token = generatePluginToken(pluginId, req.user.id);
class="code-comment">// Initialize session with MiN8T Plugin Service
const response = await fetch(class="code-string">'https:class="code-comment">//plugins.min8t.com/init', {
method: class="code-string">'POST',
headers: {
class="code-string">'Content-Type': class="code-string">'application/json',
class="code-string">'ES-PLUGIN-AUTH': token,
},
body: JSON.stringify({ pluginId, emailId }),
});
const data = await response.json();
res.json({ token, editorUrl: data.editorUrl, sessionId: data.sessionId });
});Token Format
The ES-PLUGIN-AUTH token is a Base64-encoded JSON payload with an HMAC-SHA256 signature:
Header: ES-PLUGIN-AUTH: <base64(payload)>.<base64(signature)>
Payload structure (decoded):
{
"pluginId": "string", // From pluginId
"userId": number, // End-user ID
"expires": number // Milliseconds since epoch (24hr TTL)
}Session Management
After successful initialization, a Redis-backed session is created with a configurable TTL (default: 1 hour). Sessions can be refreshed without re-initialization. If a session expires, re-call init().
Configuration Reference
All configuration parameters for the SDK init() method.
Quick Reference
const config = {
class="code-comment">// Required
pluginId: string,
apiRequestData: { emailId: string },
getAuthToken: () => string,
class="code-comment">// Optional: Core
locale?: string, class="code-comment">// Default: class="code-string">'en'
theme?: class="code-string">'light' | class="code-string">'dark', class="code-comment">// Default: class="code-string">'light'
baseUrl?: string, class="code-comment">// Default: auto-detected
class="code-comment">// Optional: Customization
customization?: {
branding?: boolean, class="code-comment">// Default: true
logoUrl?: string,
primaryColor?: string, class="code-comment">// Hex color
features?: string[], class="code-comment">// Default: [class="code-string">'editor', class="code-string">'preview', class="code-string">'export', class="code-string">'ai']
},
};
class="code-comment">// Callbacks use the event API:
min8t.on(class="code-string">'save', (result) => { /* ... */ });
min8t.on(class="code-string">'error', (err) => { /* ... */ });
min8t.on(class="code-string">'ready', () => { /* ... */ });Core Settings
| Parameter | Type | Required | Description |
|---|---|---|---|
pluginId | string | Required | Unique identifier for your plugin settings. Found under Developers Hub → Dev Platform → Projects → General tab (Plugin ID). |
apiRequestData | { emailId: string; [key: string]: any } | Required | API request data containing the email template identifier and optional custom parameters. |
getAuthToken | () => string | Required | Function that returns the ES-PLUGIN-AUTH token. Called on every API request. Can return a string or Promise<string>. See the Authentication section for token generation patterns. |
locale | string | Optional | ISO 639-1 language code for the editor UI. Determines language of labels, tooltips, and system messages. Default: 'en' |
theme | 'light' | 'dark' | Optional | UI theme preference for the embedded editor. Default: 'light' |
baseUrl | string | Optional | Base URL for the Plugin SDK API. If omitted, auto-detected using: (1) explicit baseUrl config, (2) data-base-url attribute on the script tag, (3) script src origin, (4) document.currentScript.src, (5) fallback to production CDN. Only override for self-hosted or development environments. Default: auto-detected |
Customization
| Parameter | Type | Required | Description |
|---|---|---|---|
config.customization.branding | boolean | Optional | Show or hide MiN8T branding in the editor chrome. Set to false for white-label deployments. Default: true |
config.customization.logoUrl | string | Optional | URL to a custom logo image. Replaces the MiN8T logo in the editor header. Recommended size: 120x30px. |
config.customization.primaryColor | string | Optional | Primary brand color in hex format. Applied to buttons, active states, and accent elements in the editor. |
config.customization.features | string[] | Optional | Array of enabled feature flags. Controls which capabilities are available in the embedded editor. Default: ['editor', 'preview', 'export', 'ai'] |
config.customCSS | string | Optional | Custom CSS injected into the editor iframe. Sanitized server-side to prevent XSS. Use for brand-specific typography, color overrides, or layout adjustments. |
min8t.on('save', cb), min8t.on('error', cb), etc. JavaScript API
Methods for controlling the editor programmatically via window.min8t.
init(config: PluginConfig): Promise<void>Returns: Promise<void>Initialize the plugin with configuration. Creates a session via POST /init, loads the editor iframe, and sets up postMessage communication.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
config | PluginConfig | Yes | Plugin configuration object (see Configuration Reference) |
const config = {
pluginId: class="code-string">'min8t_pk_abc123',
apiRequestData: { emailId: class="code-string">'email-123' },
getAuthToken: () => fetch(class="code-string">'/api/editor-token').then(r => r.json()).then(d => d.token),
};
await window.min8t.init(config);Response
// Resolves with void on success
// Throws on failure:
{
"error": "Invalid pluginId",
"errorType": "validation",
"isRecoverable": false
}getHtml(): Promise<{ html: string; css: string }>Returns: Promise<{ html: string; css: string }>Get the current HTML and CSS content from the editor. Communicates with the editor iframe via postMessage.
const { html, css } = await window.min8t.getHtml();
console.log(class="code-string">'HTML length:', html.length);
console.log(class="code-string">'CSS length:', css.length);Response
{
"html": "<div style=\"padding: 20px;\">...</div>",
"css": "body { font-family: Arial; } ..."
}setHtml(html: string, css: string): Promise<void>Returns: Promise<void>Set HTML and CSS content in the editor. Replaces the current template with the provided content.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
html | string | Yes | HTML content to load into the editor |
css | string | Yes | CSS styles to load into the editor |
await window.min8t.setHtml(
'<div style=class="code-string">"padding: 20px;">Hello World</div>class="code-string">',
'body { font-family: Arial; }'
);save(): Promise<SaveResponse>Returns: Promise<{ success: boolean; emailId: string; savedAt: string }>Save the current template to the backend. Retrieves HTML/CSS via getHtml(), then calls POST /plugin/save.
const result = await window.min8t.save();
console.log(class="code-string">'Saved email:', result.emailId);
console.log(class="code-string">'Saved at:', result.savedAt);Response
{
"success": true,
"emailId": "email-123",
"savedAt": "2026-02-21T14: 30: 00.000Z"
}export(format: 'html' | 'zip' | 'pdf'): Promise<ExportResponse>Returns: Promise<{ downloadUrl: string; expiresIn: number; format: string }>Export the template in the specified format. Retrieves HTML/CSS, then calls POST /export. Returns a temporary download URL.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
format | 'html' | 'zip' | 'pdf' | Yes | Export format. HTML returns compiled HTML, ZIP includes assets, PDF generates a printable version. |
const result = await window.min8t.export(class="code-string">'html');
console.log(class="code-string">'Download URL:', result.downloadUrl);
class="code-comment">// URL expires in result.expiresIn secondsResponse
{
"downloadUrl": "https://cdn.min8t.com/exports/abc123.html",
"expiresIn": 3600,
"format": "html"
}isInitialized(): booleanReturns: booleanCheck whether the plugin has been initialized. Returns true if init() has completed successfully.
if (window.min8t.isInitialized()) {
class="code-comment">// Safe to call getHtml(), save(), etc.
const { html } = await window.min8t.getHtml();
}destroy(): voidReturns: voidDestroy the plugin and clean up all resources. Removes the editor iframe, clears event listeners, and cancels pending requests.
class="code-comment">// Clean up when navigating away or unmounting
window.min8t.destroy();compile(): Promise<{ html: string; css: string; minified: boolean }>Returns: Promise<{ html: string; css: string; minified: boolean }>Compile the current template HTML/CSS with minification. Returns production-ready HTML and CSS.
const compiled = await window.min8t.compile();
console.log(class="code-string">'Compiled HTML:', compiled.html.length, class="code-string">'bytes');
console.log(class="code-string">'Minified:', compiled.minified);Response
{
"html": "<!DOCTYPE html><html>...",
"css": "body{font-family:Arial}...",
"minified": true
}preview(device?: 'desktop' | 'mobile'): Promise<{ previewUrl: string; expiresIn: number }>Returns: Promise<{ previewUrl: string; expiresIn: number }>Generate a temporary preview URL for the current template. The URL expires after 1 hour.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
device | 'desktop' | 'mobile' | No | Device viewport for the preview. Defaults to 'desktop'. |
const preview = await window.min8t.preview(class="code-string">'mobile');
window.open(preview.previewUrl);
class="code-comment">// URL expires in preview.expiresIn secondsResponse
{
"previewUrl": "https://cdn.min8t.com/preview/def456",
"expiresIn": 3600
}refreshSession(): Promise<{ sessionId: string; expiresAt: number }>Returns: Promise<{ sessionId: string; expiresAt: number }>Refresh the editor session. Regenerates the session ID (OWASP session fixation prevention) and extends the session TTL.
class="code-comment">// Refresh before the session expires (configurable TTL, default: 1 hour)
const session = await window.min8t.refreshSession();
console.log(class="code-string">'New session:', session.sessionId);
console.log(class="code-string">'Expires at:', new Date(session.expiresAt));Response
{
"sessionId": "sess_new_xyz789",
"expiresAt": 1740240600000
}undo(): Promise<UndoRedoState>Returns: Promise<UndoRedoState>Undo the last editor action. Returns the current undo/redo availability state so you can update your toolbar buttons accordingly.
const state = await window.min8t.undo();
console.log(class="code-string">'Can undo:', state.canUndo);
console.log(class="code-string">'Can redo:', state.canRedo);
class="code-comment">// Update your toolbar buttons
undoBtn.disabled = !state.canUndo;
redoBtn.disabled = !state.canRedo;Response
{
"canUndo": true,
"canRedo": true
}redo(): Promise<UndoRedoState>Returns: Promise<UndoRedoState>Redo the last undone editor action. Returns the current undo/redo availability state.
const state = await window.min8t.redo();
undoBtn.disabled = !state.canUndo;
redoBtn.disabled = !state.canRedo;Response
{
"canUndo": true,
"canRedo": false
}setMode(mode: 'visual' | 'code'): Promise<void>Returns: Promise<void>Switch the editor between visual drag-and-drop mode and HTML/CSS code editing mode. In embed mode the editor toolbar is hidden, so use this method to let users toggle between views from your own UI.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
mode | 'visual' | 'code' | Yes | Target editor mode: 'visual' for drag-and-drop, 'code' for HTML/CSS source editing |
class="code-comment">// Toggle to code view
await window.min8t.setMode(class="code-string">'code');
class="code-comment">// Toggle back to visual editor
await window.min8t.setMode(class="code-string">'visual');getMode(): Promise<EditorModeResponse>Returns: Promise<EditorModeResponse>Get the current editor mode and undo/redo availability. Useful for syncing your toolbar state with the editor.
const { mode, canUndo, canRedo } = await window.min8t.getMode();
console.log(class="code-string">'Current mode:', mode); class="code-comment">// class="code-string">'visual' or class="code-string">'code'
class="code-comment">// Sync toolbar
modeToggle.textContent = mode === class="code-string">'visual' ? class="code-string">'Code' : class="code-string">'Visual';
undoBtn.disabled = !canUndo;
redoBtn.disabled = !canRedo;Response
{
"mode": "visual",
"canUndo": true,
"canRedo": false
}toggleVersionHistory(): Promise<VersionHistoryResponse>Returns: Promise<VersionHistoryResponse>Toggle the version history sidebar open or closed. Shows a timeline of saved template versions with comparison and restore capabilities. Requires the template to have been saved at least once.
const result = await window.min8t.toggleVersionHistory();
console.log(class="code-string">'Sidebar open:', result.versionHistoryOpen);
console.log(class="code-string">'Has history:', result.hasVersionHistory);Response
{
"versionHistoryOpen": true,
"hasVersionHistory": true
}on(event: Min8tEvent, callback: Function): voidReturns: voidRegister an event listener. The callback fires each time the specified event occurs.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
event | Min8tEvent | Yes | Event name: 'initialized', 'ready', 'destroyed', 'save', 'exported', 'error', or 'authExpired' |
callback | Function | Yes | Callback function receiving the event payload |
window.min8t.on(class="code-string">'save', (result) => {
console.log(class="code-string">'Saved:', result.emailId);
});off(event: Min8tEvent, callback: Function): voidReturns: voidRemove a previously registered event listener.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
event | Min8tEvent | Yes | Event name to unsubscribe from |
callback | Function | Yes | The exact callback reference passed to on() |
const handler = (result) => console.log(result);
window.min8t.on(class="code-string">'save', handler);
class="code-comment">// Later:
window.min8t.off(class="code-string">'save', handler);once(event: Min8tEvent, callback: Function): voidReturns: voidRegister an event listener that fires only once, then automatically unregisters itself.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
event | Min8tEvent | Yes | Event name to listen for once |
callback | Function | Yes | Callback function receiving the event payload |
class="code-comment">// Wait for the editor to be ready (fires once)
window.min8t.once(class="code-string">'ready', () => {
console.log(class="code-string">'Editor is ready!');
});Events & Callbacks
Lifecycle, content, and error events from the SDK.
Lifecycle Events
initializedinit() completes successfullyFired when the SDK has completed initialization and the editor iframe is loaded.
{ sessionId: string; pluginId: string }window.min8t.on(class="code-string">'initialized', (data) => {
console.log(class="code-string">'Editor initialized, session:', data.sessionId);
});readyEditor iframe sends READY postMessage after Angular rendersFired when the editor has fully rendered and is ready to accept commands.
{ timestamp: number }window.min8t.on(class="code-string">'ready', () => {
console.log(class="code-string">'Editor is ready to accept commands');
document.getElementById(class="code-string">'save-btn').disabled = false;
});destroyeddestroy() completesFired when destroy() is called and cleanup is complete.
{}window.min8t.on(class="code-string">'destroyed', () => {
console.log(class="code-string">'Editor cleaned up');
});
window.min8t.destroy();Content Events
savesave() completes and the backend confirmsFired when a manual save completes successfully.
{ success: boolean; emailId: string; savedAt: string }window.min8t.on(class="code-string">'save', (result) => {
console.log(class="code-string">'Saved:', result.emailId, class="code-string">'at', result.savedAt);
});Export Events
exportedexport() completes successfullyFired when a template export completes.
{ format: string; downloadUrl: string }window.min8t.on(class="code-string">'exported', (result) => {
console.log(class="code-string">'Exported as', result.format, class="code-string">':', result.downloadUrl);
});Error Events
errorAny SDK method throws or the backend returns an error responseFired when an error occurs during any SDK operation.
{ error: string; code: string; errorType: string; isRecoverable: boolean }window.min8t.on(class="code-string">'error', (err) => {
if (err.isRecoverable) {
showToast(class="code-string">'Something went wrong. Please try again.');
} else {
showToast(class="code-string">'A critical error occurred. Please reload.');
}
});authExpiredBackend returns 401 with expired tokenFired when the authentication token has expired.
{ pluginId: string }window.min8t.on(class="code-string">'authExpired', (data) => {
console.log(class="code-string">'Auth expired for plugin:', data.pluginId);
class="code-comment">// Refresh the token and re-initialize
});Framework Guides
Integration examples for popular JavaScript frameworks.
<!DOCTYPE html>
<class="code-tag">html lang="en">
<class="code-tag">head>
<class="code-tag">title>Email Editor</class="code-tag">title>
<class="code-tag">script src="https://plugins.min8t.com/min8t.js"></class="code-tag">script>
</class="code-tag">head>
<class="code-tag">body>
<class="code-tag">div id="min8t-plugin" style="width: 100%; height: 600px;"></class="code-tag">div>
<class="code-tag">button id="save-btn" onclick="saveTemplate()">Save</class="code-tag">button>
<class="code-tag">button id="export-btn" onclick="exportTemplate()">Export HTML</class="code-tag">button>
<class="code-tag">script>
async function initEditor() {
await window.min8t.init({
pluginId: 'YOUR_PLUGIN_ID',
apiRequestData: { emailId: 'email-123' },
getAuthToken: () => fetch('/api/editor-token').then(r => r.json()).then(d => d.token),
theme: 'light',
});
console.log('Editor ready!');
}
async function saveTemplate() {
const result = await window.min8t.save();
alert('Saved at: ' + result.savedAt);
}
async function exportTemplate() {
const result = await window.min8t.export('html');
window.open(result.downloadUrl);
}
initEditor();
</class="code-tag">script>
</class="code-tag">body>
</class="code-tag">html>Error Reference
All error types, codes, and resolution steps.
Error Response Format
{
"error": "Error message",
"details": "Detailed description",
"errorType": "validation | auth | server | network",
"isRecoverable": true
}| Code | Type | Message | Recoverable | Cause | Resolution |
|---|---|---|---|---|---|
INVALID_CONFIG | validation | Invalid plugin configuration | Yes | Missing required fields (pluginId, emailId, or getAuthToken) in the configuration object. | Ensure all required fields are present. See Configuration Reference. |
INVALID_FORMAT | validation | Invalid export format | Yes | Export format is not one of 'html', 'zip', or 'pdf'. | Pass a valid format string: 'html', 'zip', or 'pdf'. |
INVALID_ARGS | validation | Invalid arguments: html and css must be strings | Yes | Non-string arguments passed to setHtml(). | Ensure both html and css parameters are strings. |
PLUGIN_AUTH_INVALID | auth | Invalid plugin authentication | No | The ES-PLUGIN-AUTH token is malformed, tampered with, or signed with the wrong secret. | Verify your Secret Key matches the one in Developers Hub → Dev Platform → Projects → Credentials tab. Regenerate the token if needed. |
PLUGIN_AUTH_EXPIRED | auth | Plugin authentication token expired | Yes | The ES-PLUGIN-AUTH token has exceeded its 24-hour TTL. | Generate a fresh token via your backend. The SDK will call getAuthToken() automatically on retry. |
SESSION_EXPIRED | auth | Session expired or not found | Yes | The editor session (configurable TTL, default: 1 hour) has expired in Redis. | Re-initialize the plugin with init(). A new session will be created. |
SAVE_FAILED | server | Failed to save template | Yes | The Template Service returned an error when saving. | Check that the template content is under 1MB. Retry the save operation. |
EXPORT_FAILED | server | Failed to export template | Yes | The Export Service returned an error when generating the export. | Retry the export. If the issue persists, try a different format. |
RATE_LIMIT_EXCEEDED | server | Rate limit exceeded | Yes | You have exceeded the 100 requests per 15 minutes limit. | Wait for the rate limit window to reset. Check the RateLimit-Reset header for timing. |
IFRAME_LOAD_TIMEOUT | network | Editor iframe load timeout (30s) | Yes | The editor iframe did not load within 30 seconds. | Check network connectivity. Ensure the editor URL is accessible. Retry init(). |
POSTMESSAGE_TIMEOUT | network | Editor response timeout | Yes | The editor iframe did not respond to a postMessage within 10 seconds. | The editor may be in a bad state. Try calling destroy() and then re-initializing with init(). |
NOT_INITIALIZED | network | Plugin not initialized. Call init() first. | Yes | A method was called before init() completed. | Await the init() call before calling other methods. Use isInitialized() to check state. |
Rate Limits
Request limits and response headers for the Plugin API.
Request Limits
| Tier | Limit | Window | Description |
|---|---|---|---|
| Default | 100 | per 15 minutes | Rate limit for all plugin API requests. Per-tier rate limits are planned for a future release. |
Response Headers
Rate limit information is returned in the response headers of every API call:
| Header | Description | Example |
|---|---|---|
RateLimit-Limit | Total requests allowed per window | 100 |
RateLimit-Remaining | Requests remaining in the current window | 87 |
RateLimit-Reset | Unix timestamp (seconds) when the rate limit resets | 1698163200 |
Complete Example
A self-contained HTML file that demonstrates SDK initialization, save, and export. Copy and run locally.
<!DOCTYPE html>
<class="code-tag">html lang="en">
<class="code-tag">head>
<class="code-tag">meta charset="utf-8">
<class="code-tag">title>MiN8T Editor - Minimal Example</class="code-tag">title>
<class="code-tag">style>
body { margin: 0; font-family: sans-serif; }
#controls { padding: 12px; background: #f5f5f5; display: flex; gap: 8px; }
#controls button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
#controls .primary { background: #0ea5e9; color: var(--color-text-on-accent, #fff); }
#editor-container { width: 100%; height: calc(100vh - 52px); }
</class="code-tag">style>
</class="code-tag">head>
<class="code-tag">body>
<class="code-tag">div id="controls">
<class="code-tag">button class="primary" onclick="doSave()">Save</class="code-tag">button>
<class="code-tag">button onclick="doExport('html')">Export HTML</class="code-tag">button>
<class="code-tag">button onclick="doGetHtml()">Get HTML</class="code-tag">button>
</class="code-tag">div>
<class="code-tag">div id="editor-container"></class="code-tag">div>
<class="code-tag">script src="https://plugins.min8t.com/min8t.js"></class="code-tag">script>
<class="code-tag">script>
// 1. Initialize (replace with your values)
window.min8t.init({
pluginId: 'YOUR_PLUGIN_ID',
apiRequestData: { emailId: 'demo-001' },
getAuthToken: () => fetchTokenFromBackend(),
});
// 2. Listen for events
window.min8t.on('ready', () => console.log('Editor ready'));
window.min8t.on('error', (err) => console.error('Editor error:', err));
// 3. Helper: fetch token from your backend
async function fetchTokenFromBackend() {
const res = await fetch('/api/editor-token');
const data = await res.json();
return data.token;
}
// 4. Save / Export / Get HTML
async function doSave() {
const result = await window.min8t.save();
console.log('Saved:', result);
}
async function doExport(format) {
const result = await window.min8t.export(format);
window.open(result.downloadUrl);
}
async function doGetHtml() {
const { html, css } = await window.min8t.getHtml();
console.log('HTML length:', html.length, 'CSS length:', css.length);
}
</class="code-tag">script>
</class="code-tag">body>
</class="code-tag">html>TypeScript Definitions
Full type definitions for the Plugin SDK. Copy these into your project for type safety.
class="code-comment">// @min8t.com/plugin-sdk - Type Definitions
class="code-comment">// These types ship with the SDK package in dist/index.d.ts
export interface PluginConfig {
/** Plugin identifier (from Developers Hub → Dev Platform → Projects) */
pluginId: string;
/** API request data. Must include emailId */
apiRequestData: {
emailId: string;
[key: string]: any;
};
/** Returns an ES-PLUGIN-AUTH token from your backend */
getAuthToken: () => string;
/** UI locale (default: class="code-string">'en') */
locale?: string;
/** Color theme (default: class="code-string">'light') */
theme?: class="code-string">'light' | class="code-string">'dark';
/** White-label customization */
customization?: PluginCustomization;
/** Override API base URL (auto-detected by default) */
baseUrl?: string;
}
export interface PluginCustomization {
branding?: boolean;
logoUrl?: string;
primaryColor?: string;
features?: string[];
}
export interface Min8tPlugin {
init(config: PluginConfig): Promise<void>;
getHtml(): Promise<PluginApiResponse>;
setHtml(html: string, css: string): Promise<void>;
save(): Promise<PluginSaveResponse>;
export(format: class="code-string">'html' | class="code-string">'zip' | class="code-string">'pdf'): Promise<PluginExportResponse>;
compile(): Promise<CompileResponse>;
preview(device?: class="code-string">'desktop' | class="code-string">'mobile'): Promise<PreviewResponse>;
refreshSession(): Promise<SessionRefreshResponse>;
undo(): Promise<UndoRedoState>;
redo(): Promise<UndoRedoState>;
setMode(mode: class="code-string">'visual' | class="code-string">'code'): Promise<void>;
getMode(): Promise<EditorModeResponse>;
toggleVersionHistory(): Promise<VersionHistoryResponse>;
isInitialized(): boolean;
destroy(): void;
on(event: Min8tEvent, callback: EventCallback): void;
off(event: Min8tEvent, callback: EventCallback): void;
once(event: Min8tEvent, callback: EventCallback): void;
}
class="code-comment">// ─── Response Types ───────────────────────────────────────
export interface PluginApiResponse {
html: string;
css: string;
}
export interface PluginSaveResponse {
success: boolean;
emailId: string;
savedAt: string;
}
export interface PluginExportResponse {
downloadUrl: string;
expiresIn: number;
format: class="code-string">'html' | class="code-string">'zip' | class="code-string">'pdf';
}
export interface CompileResponse {
html: string;
css: string;
minified: boolean;
}
export interface PreviewResponse {
previewUrl: string;
expiresIn: number;
}
export interface SessionRefreshResponse {
sessionId: string;
expiresAt: number;
}
export interface UndoRedoState {
canUndo: boolean;
canRedo: boolean;
}
export interface EditorModeResponse {
mode: class="code-string">'visual' | class="code-string">'code';
canUndo: boolean;
canRedo: boolean;
}
export interface VersionHistoryResponse {
versionHistoryOpen: boolean;
hasVersionHistory: boolean;
}
export interface ErrorResponse {
error: string;
code: string;
details?: string;
errorType: class="code-string">'auth' | class="code-string">'network' | class="code-string">'server' | class="code-string">'validation' | class="code-string">'rate_limit';
isRecoverable: boolean;
}
class="code-comment">// ─── Event Types ──────────────────────────────────────────
export type Min8tEvent =
| class="code-string">'initialized'
| class="code-string">'ready'
| class="code-string">'destroyed'
| class="code-string">'save'
| class="code-string">'error'
| class="code-string">'authExpired'
| class="code-string">'exported';
export type EventCallback = (data?: any) => void;
class="code-comment">// ─── Global ───────────────────────────────────────────────
declare global {
interface Window {
min8t: Min8tPlugin;
}
}Troubleshooting
Common integration issues and their solutions.
CORS Error on /init
Browser console shows "Access to fetch has been blocked by CORS policy" when calling the plugin API.
The /init endpoint must be called from your backend, not directly from the browser. Browser-to-API calls trigger CORS preflight.
Move the /init call to your backend server. Your backend calls POST /init with the ES-PLUGIN-AUTH header and returns the editorUrl to the frontend.
class="code-comment">// ❌ Wrong: browser calls API directly
fetch(class="code-string">'https:class="code-comment">//plugins.min8t.com/api/v1/init', {
headers: { class="code-string">'ES-PLUGIN-AUTH': token } class="code-comment">// Triggers CORS
});
class="code-comment">// ✅ Correct: backend proxies the call
class="code-comment">// Backend (Node.js):
app.post(class="code-string">'/api/editor-token', async (req, res) => {
const response = await fetch(class="code-string">'https:class="code-comment">//plugins.min8t.com/api/v1/init', {
method: class="code-string">'POST',
headers: { class="code-string">'ES-PLUGIN-AUTH': token, class="code-string">'Content-Type': class="code-string">'application/json' },
body: JSON.stringify({ emailId: req.body.emailId })
});
res.json(await response.json());
});CSP Blocks the Editor iframe
Editor iframe shows a blank page. Browser console shows "Refused to frame because it violates the Content-Security-Policy directive: frame-src".
Your site's Content-Security-Policy header does not allow framing the editor domain.
Add the editor domain to your CSP frame-src directive.
# Add to your server's response headers:
Content-Security-Policy: frame-src 'self' https://plugins.min8t.com;
# If using meta tag instead:
<meta http-equiv="Content-Security-Policy"
content="frame-src 'self' https://plugins.min8t.com;">Session Lost After Navigation
Editor loads initially but loses session after page navigation or refresh. API returns 404 "Session not found".
Cross-origin cookies require SameSite=None and Secure attributes. Without them, the browser drops the session cookie on subsequent requests.
Ensure your backend sets cookies with the correct attributes for cross-origin iframe usage.
class="code-comment">// Express.js example:
app.use(session({
cookie: {
sameSite: class="code-string">'none', class="code-comment">// Required for cross-origin iframe
secure: true, class="code-comment">// Required when sameSite=none
httpOnly: true,
maxAge: 3600000 class="code-comment">// 1 hour
}
}));Mixed Content Error
Editor fails to load with "Mixed Content: The page was loaded over HTTPS but requested an insecure resource".
Your HTTPS page is trying to load the editor over HTTP, or the baseUrl config points to an HTTP endpoint.
Ensure all URLs use HTTPS. If developing locally, use a local HTTPS proxy or set baseUrl to your local dev server.
class="code-comment">// ❌ Wrong
window.min8t.init({
baseUrl: class="code-string">'http:class="code-comment">//localhost:3015', // HTTP on HTTPS page
...
});
class="code-comment">// ✅ Correct: use HTTPS or omit for auto-detection
window.min8t.init({
baseUrl: class="code-string">'https:class="code-comment">//localhost:3015',
...
});Editor Buttons Not Working
Editor loads but buttons, drag-and-drop, and text editing do not respond to clicks.
A parent iframe sandbox attribute is too restrictive. The editor requires allow-scripts, allow-same-origin, and allow-forms.
If you wrap the editor in an additional iframe, ensure the sandbox attribute includes the required permissions.
<!-- Minimum required sandbox permissions -->
<class="code-tag">iframe
src="..."
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
allow="clipboard-write"
></class="code-tag">iframe>Editor Not Loading: Debug Checklist
The editor container is empty or shows a loading spinner indefinitely.
Multiple possible causes. Follow the checklist below to diagnose.
1. Check browser console for errors (F12 → Console). 2. Verify getAuthToken() returns a non-empty string. 3. Verify pluginId matches a registered plugin. 4. Verify the plugin API is reachable (curl POST /api/v1/init). 5. Check that the editor domain is not blocked by ad-blockers or firewall. 6. Verify your CSP allows frame-src for the editor domain. 7. Try in an incognito window to rule out browser extensions.
Changelog
Release history and version notes.
- added Editor control methods: undo(), redo() with canUndo/canRedo state
- added Mode switching: setMode(), getMode() for visual/code toggle
- added Version history: toggleVersionHistory() to open/close version timeline sidebar
- added New TypeScript types: UndoRedoState, EditorModeResponse, VersionHistoryResponse
- fixed Asset gallery now shows informative message in sandbox/embed mode instead of infinite loading
- added Initial SDK release with iframe-based email editor embedding
- added ES-PLUGIN-AUTH token-based authentication flow
- added Core API methods: init, getHtml, setHtml, save, export, destroy
- added Additional API methods: compile, preview, refreshSession
- added Event system with on/off/once for lifecycle, content, and error events
- added Framework guides for Vanilla JS, React, Angular, and Vue
- added Full TypeScript type definitions (dist/index.d.ts)
- added Rate limiting with draft-7 standard headers