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_…
| Method | Request |
|---|---|
tokens.create | POST /embed/v1/tokens |
designs.list | GET /embed/v1/users/{userId}/designs?limit=50&cursor=… |
designs.get | GET /embed/v1/users/{userId}/designs/{designId} |
designs.export | POST /embed/v1/designs/{designId}/export with { "user": { "id": "…" }, "merge"?, "text"?, "format"?, "fullPage"? } |
render | POST /embed/v1/render with { "doc": {…}, "designId"?, "merge"?, "text"? } |
users.erase | DELETE /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
}
| Calls | Per key, per minute |
|---|---|
| Tokens | 600 |
| Lists and reads | 600 |
| Exports | 120 |
| Renders | 120 |
| Erasures | 60 |
Over a limit, the answer is 429 rate_limited with a Retry-After header (in seconds); @lettrove/node
puts it in e.retryAfter.