Skip to main content

Server API

With the secret key, your server can work with your users' designs without the editor open: send an email on a schedule, keep your own copy, render a design you store yourself, erase someone.

import { Lettrove } from '@lettrove/node';
const lettrove = new Lettrove({ secretKey: process.env.LETTROVE_SECRET_KEY! });

Every call names the end user by your own id for them (the user.id you mint their tokens with). A design that is not that person's is design_not_found — never another person's design.

List a person's designs​

const { designs, nextCursor } = await lettrove.designs.list({ user: { id: 'u_123' }, limit: 50 });
{
"designs": [
{
"id": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"name": "October newsletter",
"mode": "email",
"revision": 12,
"createdAt": "2026-10-01T09:12:44.000Z",
"updatedAt": "2026-10-07T16:03:10.000Z"
}
],
"nextCursor": "MjAyNi0xMC0wN1QxNjowMzoxMC4wMDBafDAxSjlaUTNX…"
}

Newest first, up to limit (1–200, default 50). Pass cursor: nextCursor for the next page; nextCursor is null on the last.

Read one design​

const design = await lettrove.designs.get('01J9ZQ3W8D2K7M5T1V4XG6HB0R', { user: { id: 'u_123' } });
design.doc; // the design document — store it as it is

The same fields as in the list, plus doc, the design itself. Use it to keep your own copy (Keeping your own copy).

Export a design​

The design as last saved, as HTML to send, or as a file:

const email = await lettrove.designs.export(designId, {
user: { id: 'u_123' },
merge: { contact: { first_name: 'Ada' }, unsubscribeUrl: 'https://acme.com/u/123' }, // optional
});
// email.html, email.text, email.subject, email.preheader, email.chunks, email.mergeTags, email.warnings…

const pdf = await lettrove.designs.export(designId, { user: { id: 'u_123' }, format: 'pdf' });
// pdf.url, pdf.filename, pdf.bytes, pdf.expiresAt…

format is 'html' (the default), 'pdf', 'png' (with fullPage) or 'zip'. Every field is described in Getting designs out.

Render a design you hold​

When you keep designs yourself, render one without Lettrove storing anything:

const email = await lettrove.render({
doc: myCopy.doc, // a design document you hold
designId: myCopy.id, // optional: popups and forms are named by it
merge: { contact: { first_name: 'Ada' } }, // optional
});

Nothing is kept: no design, no export record. An image pasted straight into the design (not uploaded) is not uploaded here either; it stays inline and is named in warnings.

Erase a person​

await lettrove.users.erase('u_123');

Everything that person made — designs, restore points, images — is erased at once, with no grace period. It resolves as soon as the erasure is under way; calling it again is the same request, and an id Lettrove never saw resolves too. While it runs, no token is issued for them (user_erased). See Erasure and privacy.

Over HTTPS​

Every method is one request to https://api.lettrove.com, with your secret key:

Authorization: Bearer lt_sk_live_…
MethodRequest
tokens.createPOST /embed/v1/tokens
designs.listGET /embed/v1/users/{userId}/designs?limit=50&cursor=…
designs.getGET /embed/v1/users/{userId}/designs/{designId}
designs.exportPOST /embed/v1/designs/{designId}/export with { "user": { "id": "…" }, "merge"?, "text"?, "format"?, "fullPage"? }
renderPOST /embed/v1/render with { "doc": {…}, "designId"?, "merge"?, "text"? }
users.eraseDELETE /embed/v1/users/{userId} → 202 (erasing) or 204 (nothing to erase)

{userId} and {designId} are URL-encoded. Every request and response is in the REST reference. The same API as an OpenAPI 3.1 document, to generate a client in any language or import into Postman, Insomnia or Bruno: docs.lettrove.com/openapi/v1.json.

Errors and limits​

Every refusal carries a stable code; with @lettrove/node it is a LettroveApiError:

import { LettroveApiError } from '@lettrove/node';

try {
await lettrove.designs.get(designId, { user: { id: userId } });
} catch (e) {
if (e instanceof LettroveApiError && e.code === 'design_not_found') return null;
throw e; // e.code, e.message, e.status, e.requestId (quote it to support), e.retryAfter
}
CallsPer key, per minute
Tokens600
Lists and reads600
Exports120
Renders120
Erasures60

Over a limit, the answer is 429 rate_limited with a Retry-After header (in seconds); @lettrove/node puts it in e.retryAfter.