> Lettrove docs 1.x · https://docs.lettrove.com/docs/server/server-api

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

```ts
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

```ts
const { designs, nextCursor } = await lettrove.designs.list({ user: { id: 'u_123' }, limit: 50 });
```

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

```ts
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](/docs/data/own-copy)).

## Export a design

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

```ts
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](/docs/editor/exports).

## Render a design you hold

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

```ts
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

```ts
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](/docs/data/erasure).

## Over HTTPS

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

```http
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](/docs/api/endpoints). 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](https://docs.lettrove.com/openapi/v1.json).

## Errors and limits

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

```ts
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`.
