email-craft-sdk
v1.3.0
Published
Backend SDK for the Email Craft editor, render designs to email-safe HTML and MJML on the server, and resolve entitlements server-to-server with a secret sk_live_ key. The Admin SDK paired with the react-email-craft frontend. Like firebase-admin, for an e
Maintainers
Keywords
Readme
email-craft-sdk
The backend half of Mailkaro, the server SDK that holds your secret key and tells the editor what your users are allowed to do. It's the same split as Firebase:
| | Package | Runs on | Key it uses |
| --- | --- | --- | --- |
| Frontend | react-email-craft | your web app (browser) | public pk_live_... (or nothing) |
| Backend | email-craft-sdk (this one) | your server (Node) | secret sk_live_... |
The idea in one line: your browser never holds a secret, it asks your server "what can this user do?", and your server uses this SDK (with the secret key) to answer.
It also renders designs to email-safe HTML and MJML on the server, renderDesign(design, { fullDocument: true })
and renderMjml(design) are pure functions (no headless browser, no key required).
Try it live
Open the Next.js starter (editor + this SDK) in your browser:
Guide: Render MJML and email-safe HTML on the server in Node.js
Requirements
- Node.js 18 or newer (it uses the built-in
fetch). - A secret server key that looks like
sk_live_..., from your Mailkaro dashboard. Keep it in your backend environment only, never send it to the browser.
Install
npm install email-craft-sdk
# or: pnpm add email-craft-sdk / yarn add email-craft-sdkQuick start
Three lines on your backend, create the client, resolve the runtime, send it to your editor:
import { Mailkaro } from "email-craft-sdk";
const mailkaro = Mailkaro.fromEnv(); // reads MAILKARO_SERVER_KEY (sk_live_…) from your env
// inside an authenticated backend route:
const runtime = await mailkaro.resolveRuntime(); // → { plan, entitlements, capabilities, merge_tags, … }
res.json(runtime); // hand it to your frontend <EmailEditor runtimeUrl=… />That's the whole backend. Everything below is optional detail. Full runnable versions are in A complete Express route and the combined example.
The flow (how the two packages work together)
1. Browser (react-email-craft) ──▶ 2. YOUR backend route (email-craft-sdk) ──▶ 3. Mailkaro API
"what can I do?" checks the user is logged in, validates the
then resolveRuntime() secret sk_live_ key
◀────────────────────────────────── returns the runtime ◀────────────────────
editor unlocks the allowed featuresYour frontend points the editor at your endpoint (no secret in the browser):
import EmailEditor from "react-email-craft";
<EmailEditor runtimeUrl="/mailkaro/embed-runtime" />;A complete Express route
import express from "express";
import { Mailkaro } from "email-craft-sdk";
const app = express();
// Create it once and reuse it. Reads MAILKARO_SERVER_KEY / MAILKARO_API_URL / MAILKARO_ORG_ID
// from your environment, and auto-detects features from your env (S3 bucket, AI key, …).
const mailkaro = Mailkaro.fromEnv();
// ...or configure it by hand:
// const mailkaro = new Mailkaro({
// serverKey: process.env.MAILKARO_SERVER_KEY!, // sk_live_… (backend only)
// apiUrl: process.env.MAILKARO_API_URL, // optional, defaults to https://api.mailkaro.com
// orgId: process.env.MAILKARO_ORG_ID, // optional, for multi-tenant accounts
// });
// The endpoint your editor calls. `requireAuth` is YOUR existing login check.
app.get("/mailkaro/embed-runtime", requireAuth, async (req, res) => {
const runtime = await mailkaro.resolveRuntime();
res.json(runtime); // { plan, entitlements, merge_tags, capabilities, expired, free }
});
app.listen(3000);That's the whole backend. Your frontend uses runtimeUrl="/mailkaro/embed-runtime" and the editor
unlocks whatever the runtime says.
Every key, one by one (what it is, and what it turns on)
Each key is independent. Only the first one is required, add the others only if you want that feature. For every key below you'll see the env line, what it means, and a tiny standalone example.
1. MAILKARO_SERVER_KEY, your secret key (required)
MAILKARO_SERVER_KEY=sk_live_xxxxxxxxxxxxxMeaning: this proves the request is coming from you. The SDK uses it to ask Mailkaro what plan and features your account has. Never send it to the browser.
// This one key alone is enough to know your plan:
const mailkaro = new Mailkaro({ serverKey: process.env.MAILKARO_SERVER_KEY! });
const runtime = await mailkaro.resolveRuntime(); // → { plan: "pro", entitlements, … }2. MAILKARO_API_URL, which Mailkaro server to talk to (optional)
MAILKARO_API_URL=https://api.mailkaro.com # this is the default, only change it for self-hostingMeaning: the address of the Mailkaro API. Leave it out and it uses https://api.mailkaro.com.
3. MAILKARO_ORG_ID, which workspace (optional, for multi-tenant)
MAILKARO_ORG_ID=org_xxxxxxxxMeaning: if your account has several organizations/workspaces, this picks which one to resolve. Most single-company setups don't need it.
4. MAILKARO_S3_BUCKET (+ AWS creds), turn on S3 uploads (optional, free on any plan)
MAILKARO_S3_BUCKET=my-bucket
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
AWS_REGION=us-east-1Meaning: shows an S3 upload/browse tab in the editor, and files go to your bucket. Because you supply the storage, it works on any plan, free included.
const mailkaro = Mailkaro.fromEnv();
const runtime = await mailkaro.resolveRuntime();
runtime.capabilities.s3; // → true (the editor now shows the S3 tab)5. An AI key, turn on AI writing (optional, free on any plan)
OPEN_AI_KEY=sk-xxxxxxxx # or: CLAUDE_AI_KEY=xxxxxxxx or: GEMINI_AI_KEY=xxxxxxxxMeaning: enables the editor's AI features (e.g. "write subject lines"). The key stays on your server, the browser calls your backend, your backend calls the AI provider.
runtime.capabilities.ai; // → true
// and you can run AI on the server directly:
await mailkaro.ai("generateSubjectLines", { prompt: "Summer sale email", count: 5 });6. MAILKARO_GOOGLE_CLIENT_ID, turn on Google Drive (optional, free on any plan)
MAILKARO_GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.comMeaning: shows the "Connect Google Drive" picker so users can pull images from Drive. Only the public client id reaches the browser, the client secret stays on your backend.
Full example, all the keys, working together
Here's a complete setup with every key turned on. Three files: your .env, your Node backend, and
your React page. Every line is explained.
Step 1, .env (backend only; never shipped to the browser):
# ── Required ──────────────────────────────────────────────
MAILKARO_SERVER_KEY=sk_live_xxxxxxxx # your secret key, proves the server is you
# ── Optional ──────────────────────────────────────────────
MAILKARO_ORG_ID=org_xxxxxxxx # your workspace (only for multi-tenant accounts)
# Turn on S3 uploads (free), files go to YOUR bucket
MAILKARO_S3_BUCKET=my-bucket
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
AWS_REGION=us-east-1
# Turn on AI writing (free), pick ONE provider
OPEN_AI_KEY=sk-xxxxxxxx
# Turn on Google Drive picker (free)
MAILKARO_GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.comStep 2, your Node backend (server.js):
import express from "express";
import { Mailkaro } from "email-craft-sdk";
const app = express();
// Build the SDK from ALL the env keys above at once.
// It reads the server key, and auto-detects S3 + AI + Google from the env, nothing else to wire up.
const mailkaro = Mailkaro.fromEnv();
// This is the single endpoint your editor will call.
// `requireAuth` is YOUR normal login check, only logged-in users get past it.
app.get("/mailkaro/embed-runtime", requireAuth, async (req, res) => {
// Ask Mailkaro: what plan is this account on, and which features are on?
const runtime = await mailkaro.resolveRuntime();
// Send the answer to the browser. It's just on/off flags + public config, no secrets inside.
res.json(runtime);
});
app.listen(3000, () => console.log("backend ready on http://localhost:3000"));Step 3, your React page (Editor.jsx):
import EmailEditor from "react-email-craft";
import { useRef } from "react";
export function Editor() {
const ref = useRef(null);
// When the user clicks Save, ask the editor for the finished email.
const save = () =>
ref.current.editor.exportHtml(({ design, html }) => {
saveToMyDatabase(design); // store the design so the user can edit it again later
sendWithMyEmailProvider(html); // or send the HTML right away
});
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<button onClick={save}>Save</button>
<EmailEditor
ref={ref}
// Point at YOUR backend route from Step 2, no secret in the browser.
// The editor calls it, gets the runtime, and turns on S3 + AI + Google automatically.
runtimeUrl="/mailkaro/embed-runtime"
style={{ flex: 1 }}
/>
</div>
);
}What happens when it runs: the editor loads → it calls your /mailkaro/embed-runtime route → your
backend replies with the plan + features → the editor opens with S3, AI, and Google Drive all
enabled, powered entirely by your own keys, and the browser never saw a single secret.
Prefer the 2-minute version? If you don't need your own S3/AI and just want the editor, skip the backend entirely: put a publishable token straight on the component,
<EmailEditor token="pk_live_..." />, and you're done. Add the backend later when you want BYO infrastructure.
Bring your own S3 / AI / Google, free on any plan
Anything you provide runs on any plan, free included, Mailkaro only gates the features Mailkaro itself hosts. Because the credentials live in your backend, the browser never sees them.
| You set in your backend | The editor shows | Costs a paid plan? |
| --- | --- | --- |
| MAILKARO_S3_BUCKET (+ AWS creds) | S3 asset browser/upload | ❌ No, free |
| an AI key (OPEN_AI_KEY, …) | AI writing features | ❌ No, free |
| MAILKARO_GOOGLE_CLIENT_ID | Google Drive picker | ❌ No, free |
resolveRuntime() returns a capabilities object with on/off flags and non-secret config (like
font names), never a secret. The editor renders those, and for the privileged work (signing an
upload, generating AI text) it calls back to your backend, so no credential ever reaches the
browser.
Per-tenant S3 folders
Keep each customer's uploads in their own folder:
const prefix = mailkaro.s3AssetPrefix(user.organization_id); // e.g. "mailkaro/<org-id>/"AI on your server
const result = await mailkaro.ai("generateSubjectLines", { prompt: "Summer sale email", count: 5 });Supported methods: rewriteText, generateSubjectLines, generateAltText, generateDesignFromPrompt and editDesign
(the editor's chat assistant). Method names are camelCase. For editDesign pass { message, history?, design_summary };
it returns { operations, summary } and the editor applies the operations itself. Any other method throws.
Render an email on the server (no browser needed)
email-craft-sdk bundles the renderer, so you can turn a saved design into HTML on the backend, handy
for sending the actual email:
import { renderDesign } from "email-craft-sdk";
const html = renderDesign(design, { mergeTags, fullDocument: true });
// hand `html` to your email provider (SES, SendGrid, Postmark, …)API reference
Mailkaro.fromEnv()
Builds the client from your environment variables (see the table above). The easiest way to start.
new Mailkaro({ serverKey, apiUrl?, orgId?, capabilities?, ai?, fetch? })
Build it by hand. serverKey is required. fetch is optional (defaults to the global fetch).
createMailkaro(options) does the same thing if you prefer a factory function.
resolveRuntime({ project? }): Promise<MailkaroRuntime>
The main method. Returns { plan, entitlements, merge_tags, capabilities, expired, free } for your
key. Never throws, if the key is invalid, expired, or Mailkaro is unreachable, it safely returns
the free runtime, so a paid tier can never leak by accident.
isPaid(project?): Promise<boolean>
Shortcut: true when the plan is pro, ai_pro, or enterprise.
ai(method, input): Promise<unknown>
Run an AI action on your server using your configured AI key.
s3AssetPrefix(scopeId): string
The isolated S3 key prefix for one tenant (e.g. "mailkaro/<org-id>/").
renderDesign(design, options?)
Turn a design JSON into HTML on the server.
Why this is safe
- The secret never reaches the browser,
sk_live_...lives only in your backend env. - Your backend authorizes the request using your own login/session before resolving anything.
- Enforcement is server-side,
resolveRuntime()never self-grants; an invalid/expired/unreachable key falls back to free. Paid features cannot leak.
Frontend companion
Pair this with react-email-craft in your web app.
Point the editor at your backend with runtimeUrl="/mailkaro/embed-runtime" and you're done.
License
MIT
