Backend API
Read your users' templates and rendered HTML from your server with the Plugin ID and Secret.
The editor never talks to your page. To find out what a user has built, your server calls the Backend API. It lists the users who have saved work, their templates, and the rendered HTML of any template.
Authentication
Send the Plugin ID and Secret as headers on every request:
X-Plugin-Id: 9f3c1e7a-4b21-4c8e-9a77-1d2f5b6c8e10
X-Plugin-Secret: your_plugin_secret- Requests must come from a server. The API has no CORS headers, and any request that carries the secret together with a browser
Originheader is refused. - Credentials in the query string are refused with
credentials_in_query.
Endpoints
| Method | Path | Returns |
|---|---|---|
GET | /plugin/v1/backend/users | Users who have saved at least one template, with counts and activity dates. |
GET | /plugin/v1/backend/templates | Templates, filterable by external_user_id, search, updated_after and updated_before. |
GET | /plugin/v1/backend/templates/{id} | One template's metadata. |
GET | /plugin/v1/backend/templates/{id}/html | The template's rendered HTML. |
Paths are relative to https://api.maileditor.net/api. Lists are paginated with per_page (default 25, maximum 100) and page. Full request and response shapes: Backend API reference.
Examples
curl "https://api.maileditor.net/api/plugin/v1/backend/templates?external_user_id=customer-user-4471&per_page=100" \
-H "Accept: application/json" \
-H "X-Plugin-Id: $MAILEDITOR_PLUGIN_ID" \
-H "X-Plugin-Secret: $MAILEDITOR_PLUGIN_SECRET"A Next.js route that proxies the templates list to your own front end. The secret stays on the server.
export async function GET(request: Request) {
const user = new URL(request.url).searchParams.get('user')
if (!user) return Response.json({ error: 'user is required' }, { status: 400 })
const url = new URL(`${process.env.MAILEDITOR_API_BASE_URL}/plugin/v1/backend/templates`)
url.searchParams.set('external_user_id', user)
url.searchParams.set('per_page', '100')
const upstream = await fetch(url, {
headers: {
Accept: 'application/json',
'X-Plugin-Id': process.env.MAILEDITOR_PLUGIN_ID ?? '',
'X-Plugin-Secret': process.env.MAILEDITOR_PLUGIN_SECRET ?? '',
},
cache: 'no-store',
})
if (!upstream.ok) return Response.json({ error: 'upstream error' }, { status: 502 })
const body = await upstream.json()
return Response.json(body.result.templates.data)
}A template in the list looks like this:
{
"id": 4821,
"name": "September newsletter",
"slug": "september-newsletter",
"thumbnail": "https://…/thumbnails/4821.png",
"external_user_id": "customer-user-4471",
"created_at": "2026-08-20T09:14:02Z",
"updated_at": "2026-08-28T16:40:11Z"
}Getting the HTML
curl "https://api.maileditor.net/api/plugin/v1/backend/templates/4821/html" \
-H "X-Plugin-Id: $MAILEDITOR_PLUGIN_ID" \
-H "X-Plugin-Secret: $MAILEDITOR_PLUGIN_SECRET"The HTML is a string in result.html. source is media when the last save has been rendered and draft while a save from the last few seconds is still being processed. Either is safe to use.
Keeping in sync
There are no webhooks. Two patterns work well:
- Read on demand. Fetch the user's templates when your page that lists them loads, without caching, so a save made in the editor is visible immediately.
- Poll with
updated_after. Periodically list templates changed since your last check and mirror them into your own database.
Limits and errors
Backend requests are limited to 60 per minute and 2000 per day per plugin. Above that the API answers 429 with a Retry-After header.
| Status | result.error | Meaning |
|---|---|---|
| 401 | credentials_required | One or both headers missing. |
| 401 | invalid_credentials | The pair does not match a plugin. |
| 401 | credentials_in_query | Credentials were sent as query parameters. |
| 409 | plugin_not_provisioned | The plugin is still being set up. Retry shortly. |
| 404 | template_not_found | No template with that id belongs to this plugin. |

.png)