Plugin SDK Documentation

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.

bash
# npm
npm install @min8t.com/plugin-sdk

# yarn
yarn add @min8t.com/plugin-sdk

# pnpm
pnpm add @min8t.com/plugin-sdk

Usage in your code:

javascript
// 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.

html
<!-- 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.

html
<!-- 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

Format:min8t_pk_ followed by 48 hex characters (e.g. min8t_pk_a1b2c3d4e5f6...)
Visibility: Public. Safe to include in frontend JavaScript code
Usage: Passed to `init({ pluginId })` in the browser. Identifies which project the editor session belongs to.
Where to find: Developers Hub → Dev Platform → Projects → select your project → Credentials tab. The Plugin ID is always visible (displayed masked as min8t_pk_a1b2****).

Secret Key

Format:min8t_sk_ followed by 48 hex characters (e.g. min8t_sk_x9y8z7w6v5u4...)
Visibility: Private. NEVER expose in frontend code, HTML, or client-side bundles
Usage: Used on your backend server to generate HMAC-SHA256 signed tokens (ES-PLUGIN-AUTH header). This proves to the MiN8T Plugin Service that the request came from your authorized server.
Where to find: Developers Hub → Dev Platform → Projects → select your project → Credentials tab. The Secret Key is shown only once when the project is created or when you rotate credentials. Copy it immediately.

Setting Up Your Secret Key

Store your Secret Key as an environment variable on your backend server. Never commit it to source control.

bash
# .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_here
Important: Your Secret Key is shown only once when first created or rotated. If you lose it, go to the Credentials tab and click "Rotate Credentials" to generate a new Plugin ID and Secret Key pair. This immediately invalidates all existing tokens signed with the old key. Your backend must be updated with the new credentials.

Accessing the Secret Key in Your Backend Code

multi-language
// 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.

What you'll build
your-app.com/editor
Your App
MiN8T Editor (iframe)
1

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.

html
# 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>
2

Configure

Create a configuration object with your plugin ID, email ID, and auth token provider.

javascript
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',
};
3

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.

javascript
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() implementation
4

Initialize

Call `init()` to start the editor. The SDK creates an iframe, authenticates, and loads the email template.

javascript
class="code-comment">// Initialize the editor
await window.min8t.init(config);
console.log(class="code-string">'Editor is ready!');
5

Use

The editor mounts in the `#min8t-plugin` container. Use the JavaScript API to save, export, or retrieve content.

javascript
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.

Security Warning: Never expose your Secret Key (MIN8T_SECRET_KEY) in client-side code. Token generation must happen on your backend server.

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

Node.js
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

typescript
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

ParameterTypeRequiredDescription
pluginIdstringRequiredUnique identifier for your plugin settings. Found under Developers Hub → Dev Platform → Projects → General tab (Plugin ID).
apiRequestData{ emailId: string; [key: string]: any }RequiredAPI request data containing the email template identifier and optional custom parameters.
getAuthToken() => stringRequiredFunction 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.
localestringOptionalISO 639-1 language code for the editor UI. Determines language of labels, tooltips, and system messages. Default: 'en'
theme'light' | 'dark'OptionalUI theme preference for the embedded editor. Default: 'light'
baseUrlstringOptionalBase 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

ParameterTypeRequiredDescription
config.customization.brandingbooleanOptionalShow or hide MiN8T branding in the editor chrome. Set to false for white-label deployments. Default: true
config.customization.logoUrlstringOptionalURL to a custom logo image. Replaces the MiN8T logo in the editor header. Recommended size: 120x30px.
config.customization.primaryColorstringOptionalPrimary brand color in hex format. Applied to buttons, active states, and accent elements in the editor.
config.customization.featuresstring[]OptionalArray of enabled feature flags. Controls which capabilities are available in the embedded editor. Default: ['editor', 'preview', 'export', 'ai']
config.customCSSstringOptionalCustom CSS injected into the editor iframe. Sanitized server-side to prevent XSS. Use for brand-specific typography, color overrides, or layout adjustments.
Note: Callbacks have been replaced by the Event API. Use 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

NameTypeRequiredDescription
configPluginConfigYesPlugin configuration object (see Configuration Reference)
javascript
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

json
// 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.

javascript
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

json
{
  "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

NameTypeRequiredDescription
htmlstringYesHTML content to load into the editor
cssstringYesCSS styles to load into the editor
javascript
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.

javascript
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

json
{
  "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

NameTypeRequiredDescription
format'html' | 'zip' | 'pdf'YesExport format. HTML returns compiled HTML, ZIP includes assets, PDF generates a printable version.
javascript
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 seconds

Response

json
{
  "downloadUrl": "https://cdn.min8t.com/exports/abc123.html",
  "expiresIn": 3600,
  "format": "html"
}
isInitialized(): booleanReturns: boolean

Check whether the plugin has been initialized. Returns true if init() has completed successfully.

javascript
if (window.min8t.isInitialized()) {
  class="code-comment">// Safe to call getHtml(), save(), etc.
  const { html } = await window.min8t.getHtml();
}
destroy(): voidReturns: void

Destroy the plugin and clean up all resources. Removes the editor iframe, clears event listeners, and cancels pending requests.

javascript
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.

javascript
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

json
{
  "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

NameTypeRequiredDescription
device'desktop' | 'mobile'NoDevice viewport for the preview. Defaults to 'desktop'.
javascript
const preview = await window.min8t.preview(class="code-string">'mobile');
window.open(preview.previewUrl);
class="code-comment">// URL expires in preview.expiresIn seconds

Response

json
{
  "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.

javascript
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

json
{
  "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.

javascript
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

json
{
  "canUndo": true,
  "canRedo": true
}
redo(): Promise<UndoRedoState>Returns: Promise<UndoRedoState>

Redo the last undone editor action. Returns the current undo/redo availability state.

javascript
const state = await window.min8t.redo();
undoBtn.disabled = !state.canUndo;
redoBtn.disabled = !state.canRedo;

Response

json
{
  "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

NameTypeRequiredDescription
mode'visual' | 'code'YesTarget editor mode: 'visual' for drag-and-drop, 'code' for HTML/CSS source editing
javascript
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.

javascript
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

json
{
  "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.

javascript
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

json
{
  "versionHistoryOpen": true,
  "hasVersionHistory": true
}
on(event: Min8tEvent, callback: Function): voidReturns: void

Register an event listener. The callback fires each time the specified event occurs.

Parameters

NameTypeRequiredDescription
eventMin8tEventYesEvent name: 'initialized', 'ready', 'destroyed', 'save', 'exported', 'error', or 'authExpired'
callbackFunctionYesCallback function receiving the event payload
javascript
window.min8t.on(class="code-string">'save', (result) => {
  console.log(class="code-string">'Saved:', result.emailId);
});
off(event: Min8tEvent, callback: Function): voidReturns: void

Remove a previously registered event listener.

Parameters

NameTypeRequiredDescription
eventMin8tEventYesEvent name to unsubscribe from
callbackFunctionYesThe exact callback reference passed to on()
javascript
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: void

Register an event listener that fires only once, then automatically unregisters itself.

Parameters

NameTypeRequiredDescription
eventMin8tEventYesEvent name to listen for once
callbackFunctionYesCallback function receiving the event payload
javascript
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 successfully

Fired when the SDK has completed initialization and the editor iframe is loaded.

Payload:{ 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 renders

Fired when the editor has fully rendered and is ready to accept commands.

Payload:{ 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() completes

Fired when destroy() is called and cleanup is complete.

Payload:{}
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 confirms

Fired when a manual save completes successfully.

Payload:{ 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 successfully

Fired when a template export completes.

Payload:{ 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 response

Fired when an error occurs during any SDK operation.

Payload:{ 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 token

Fired when the authentication token has expired.

Payload:{ 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.

html
<!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>
Note: The SDK auto-creates a container if `#min8t-plugin` is not found, but specifying one gives you control over size and placement.

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
}
CodeTypeMessageRecoverableCauseResolution
INVALID_CONFIGvalidationInvalid plugin configurationYesMissing required fields (pluginId, emailId, or getAuthToken) in the configuration object.Ensure all required fields are present. See Configuration Reference.
INVALID_FORMATvalidationInvalid export formatYesExport format is not one of 'html', 'zip', or 'pdf'.Pass a valid format string: 'html', 'zip', or 'pdf'.
INVALID_ARGSvalidationInvalid arguments: html and css must be stringsYesNon-string arguments passed to setHtml().Ensure both html and css parameters are strings.
PLUGIN_AUTH_INVALIDauthInvalid plugin authenticationNoThe 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_EXPIREDauthPlugin authentication token expiredYesThe 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_EXPIREDauthSession expired or not foundYesThe editor session (configurable TTL, default: 1 hour) has expired in Redis.Re-initialize the plugin with init(). A new session will be created.
SAVE_FAILEDserverFailed to save templateYesThe Template Service returned an error when saving.Check that the template content is under 1MB. Retry the save operation.
EXPORT_FAILEDserverFailed to export templateYesThe Export Service returned an error when generating the export.Retry the export. If the issue persists, try a different format.
RATE_LIMIT_EXCEEDEDserverRate limit exceededYesYou 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_TIMEOUTnetworkEditor iframe load timeout (30s)YesThe editor iframe did not load within 30 seconds.Check network connectivity. Ensure the editor URL is accessible. Retry init().
POSTMESSAGE_TIMEOUTnetworkEditor response timeoutYesThe 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_INITIALIZEDnetworkPlugin not initialized. Call init() first.YesA 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

TierLimitWindowDescription
Default100per 15 minutesRate 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:

HeaderDescriptionExample
RateLimit-LimitTotal requests allowed per window100
RateLimit-RemainingRequests remaining in the current window87
RateLimit-ResetUnix timestamp (seconds) when the rate limit resets1698163200
Per-tier rate limits are planned. See the Plugin Monetization spec for details.

Complete Example

A self-contained HTML file that demonstrates SDK initialization, save, and export. Copy and run locally.

html
<!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.

typescript
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

Problem

Browser console shows "Access to fetch has been blocked by CORS policy" when calling the plugin API.

Cause

The /init endpoint must be called from your backend, not directly from the browser. Browser-to-API calls trigger CORS preflight.

Solution

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.

javascript
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

Problem

Editor iframe shows a blank page. Browser console shows "Refused to frame because it violates the Content-Security-Policy directive: frame-src".

Cause

Your site's Content-Security-Policy header does not allow framing the editor domain.

Solution

Add the editor domain to your CSP frame-src directive.

bash
# Add to your server's response headers:
Content-Security-Policy: frame-src &#x27;self' https://plugins.min8t.com;

# If using meta tag instead:
<meta http-equiv="Content-Security-Policy"
      content="frame-src &#x27;self' https://plugins.min8t.com;">

Session Lost After Navigation

Problem

Editor loads initially but loses session after page navigation or refresh. API returns 404 "Session not found".

Cause

Cross-origin cookies require SameSite=None and Secure attributes. Without them, the browser drops the session cookie on subsequent requests.

Solution

Ensure your backend sets cookies with the correct attributes for cross-origin iframe usage.

javascript
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

Problem

Editor fails to load with "Mixed Content: The page was loaded over HTTPS but requested an insecure resource".

Cause

Your HTTPS page is trying to load the editor over HTTP, or the baseUrl config points to an HTTP endpoint.

Solution

Ensure all URLs use HTTPS. If developing locally, use a local HTTPS proxy or set baseUrl to your local dev server.

javascript
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

Problem

Editor loads but buttons, drag-and-drop, and text editing do not respond to clicks.

Cause

A parent iframe sandbox attribute is too restrictive. The editor requires allow-scripts, allow-same-origin, and allow-forms.

Solution

If you wrap the editor in an additional iframe, ensure the sandbox attribute includes the required permissions.

html
<!-- 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

Problem

The editor container is empty or shows a loading spinner indefinitely.

Cause

Multiple possible causes. Follow the checklist below to diagnose.

Solution

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.

vv1.1.02026-02-24
  • 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
vv1.0.02026-02-21
  • 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
SDK v1.0.0