REST API
Base address https://api.lettrove.com. Every request carries your secret key, from your server only:
Authorization: Bearer lt_sk_live_…
Content-Type: application/json
Conventions (errors, ids, times, limits) are on the API overview. Path parameters are URL-encoded. Request bodies are strict: a field the table does not list is refused, naming it. Answers may gain fields within v1: ignore ones you do not know.
The OpenAPI document
This page is generated from the API's OpenAPI 3.1 document, which is generated from the server's own schemas and tested against every real answer. Use it to generate a client in any language, or import it into Postman, Insomnia or Bruno:
It covers these endpoints and the webhook events. It has no "try it" console on purpose: the API takes your secret key and answers no browser.
Create a token
POST /embed/v1/tokens
Exchange your secret key for a short-lived editor token for one person on one site. Your page asks your server for it (getToken), and the editor opens with it. Never cached.
Limit: 600 requests a minute per key. Operation id: createToken.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user | object | Yes | The person the editor opens for. |
user.id | string | Yes | Your own id for the person: whatever you chose, up to 256 characters. Never a name or an email. |
origin | string | Yes | The site the editor opens on, exactly as a browser states it: scheme, host and port, no path. It must be one of the project's allowed sites; a test key also opens on localhost. Length 1 to 2048. |
ttl | integer | Seconds the token lasts, 60 to 900. Default 900. |
{
"user": {
"id": "u_123"
},
"origin": "https://app.acme.com",
"ttl": 900
}
200 The token.
| Field | Type | Description |
|---|---|---|
token | string | Hand it to the page, which passes it to the editor. Never store it. |
expiresAt | string (date-time) | When it stops working. The page asks your server for a new one before then. |
{
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-07T16:15:00.000Z"
}
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
400 | request_invalid: The body does not match; the message names the field. |
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.origin_not_allowed: The origin is not one of the project's allowed sites for this key's environment. |
409 | user_erased: This person is being erased. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |
List a person's designs
GET /embed/v1/users/{userId}/designs
One person's designs, newest first, a page at a time. A person Lettrove has never seen has none.
Limit: 600 requests a minute per key. Operation id: listDesigns.
| Parameter | In | Type | Description |
|---|---|---|---|
userId | path | string | Your own id for the person: whatever you chose, up to 256 characters. Never a name or an email. |
limit | query | integer | Designs per page, 1 to 200. Default 50. |
cursor | query | string | The nextCursor of the previous page. |
200 A page of designs.
| Field | Type | Description |
|---|---|---|
designs | object[] | Newest first. Empty for a person Lettrove has never seen. |
designs[].id | string | The id Lettrove gave the design: 26 letters and digits. |
designs[].name | string | The name the person gave it. |
designs[].mode | email | page | popup | document | What the design is: email, page, popup or document. A project makes one mode. |
designs[].revision | integer | The saved revision; only ever goes up. |
designs[].createdAt | string (date-time) | ISO 8601, UTC. |
designs[].updatedAt | string (date-time) | ISO 8601, UTC. |
nextCursor | string or null | Pass as cursor for the next page; null on the last. |
{
"designs": [
{
"id": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"name": "October newsletter",
"mode": "email",
"revision": 12,
"createdAt": "2026-10-07T16:03:10.000Z",
"updatedAt": "2026-10-07T16:03:10.000Z"
}
],
"nextCursor": null
}
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
400 | request_invalid: The user id or the cursor is not valid. |
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project. |
409 | user_erased: This person is being erased. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |
Get a design
GET /embed/v1/users/{userId}/designs/{designId}
One design as last saved, with its document: your own copy.
Limit: 600 requests a minute per key. Operation id: getDesign.
| Parameter | In | Type | Description |
|---|---|---|---|
userId | path | string | Your own id for the person: whatever you chose, up to 256 characters. Never a name or an email. |
designId | path | string | The id Lettrove gave the design: 26 letters and digits. |
200 The design.
| Field | Type | Description |
|---|---|---|
id | string | The id Lettrove gave the design: 26 letters and digits. |
name | string | The name the person gave it. |
mode | email | page | popup | document | What the design is: email, page, popup or document. A project makes one mode. |
revision | integer | The saved revision; only ever goes up. |
createdAt | string (date-time) | ISO 8601, UTC. |
updatedAt | string (date-time) | ISO 8601, UTC. |
doc | object | The design document. Store it as it is; what you may rely on inside it is on docs.lettrove.com/docs/1/api/design-document. |
{
"id": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"name": "October newsletter",
"mode": "email",
"revision": 12,
"createdAt": "2026-10-07T16:03:10.000Z",
"updatedAt": "2026-10-07T16:03:10.000Z",
"doc": {}
}
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project. |
404 | design_not_found: No design with that id is this person's. Another person's design is not found either, never refused. |
409 | user_erased: This person is being erased. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |
Export a design
POST /embed/v1/designs/{id}/export
One person's design as last saved: as HTML (the default), or as a PDF, PNG or ZIP file kept for a URL. Recorded, and sent as an export.created webhook.
Limit: 120 requests a minute per key. Operation id: exportDesign.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | The id Lettrove gave the design: 26 letters and digits. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
user | object | Yes | Whose design it is. Another person's design is not found. |
user.id | string | Yes | Your own id for the person: whatever you chose, up to 256 characters. Never a name or an email. |
merge | object | Merge tags to fill. A tag with no value stays as written and is listed in mergeTags. | |
merge.contact | object | The values for {{contact.<field>}} tags, by field. | |
merge.unsubscribeUrl | string | Your unsubscribe link, for {{unsubscribe_url}}. Left out, the tag stays for your sending provider to fill. | |
merge.preferencesUrl | string | Your preferences link, for {{preferences_url}}. Left out, the tag stays. | |
text | object | How the plain-text version is written. | |
text.links | boolean | Each link's address after its text. | |
text.images | boolean | Images' alt text. | |
text.preheader | boolean | The preheader first. | |
format | html | zip | pdf | png | html (the default) answers with the HTML; pdf, png or zip with a file kept for a URL. | |
fullPage | boolean | With png: the whole design (true, the default) or the first screen only. |
{
"user": {
"id": "u_123"
},
"merge": {
"contact": {
"first_name": "Ada"
},
"unsubscribeUrl": "https://acme.com/u/123"
},
"format": "html"
}
200 The HTML, or the file (format is set on a file).
As HTML
| Field | Type | Description |
|---|---|---|
exportId | string | The export's record. |
designId | string | The id Lettrove gave the design: 26 letters and digits. |
revision | integer | The saved revision; only ever goes up. |
mode | email | page | popup | document | What the design is: email, page, popup or document. A project makes one mode. |
html | string | The whole document, ready to send or serve. |
chunks | object | The HTML in pieces, to place it inside a page of your own. |
chunks.body | string | What goes inside <body>, its scripts taken out. |
chunks.css | string | Every stylesheet the design carries, in order. |
chunks.js | string | Every script it runs, in order; empty for an email. |
chunks.fonts | string[] | The web-font stylesheets it links to, by URL. |
text | string | The plain-text version (the text/plain part), merged the same way. |
subject | string | The subject line, merged. |
preheader | string | The preview text after the subject, merged. |
mergeTags | string[] | Tags still in the HTML, text, subject or preheader, exactly as written: yours to fill before it goes to anyone. |
rendererVersion | integer | Which renderer made it. Goes up when the output changes. |
bytes | integer | Size of html in bytes. |
warnings | string[] | Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline. |
design | object | The design document exported. Store it as it is. |
{
"exportId": "01J9ZR4C2N8B6V0X3M7K5T9QWE",
"designId": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"revision": 12,
"mode": "email",
"html": "<!doctype html>…",
"chunks": {
"body": "…",
"css": "…",
"js": "",
"fonts": []
},
"text": "Hi Ada, your order has shipped…",
"subject": "Your order has shipped",
"preheader": "It arrives Thursday.",
"mergeTags": [],
"rendererVersion": 7,
"bytes": 38211,
"warnings": [],
"design": {}
}
As a file
| Field | Type | Description |
|---|---|---|
exportId | string | The export's record. |
designId | string | The id Lettrove gave the design: 26 letters and digits. |
revision | integer | The saved revision; only ever goes up. |
mode | email | page | popup | document | What the design is: email, page, popup or document. A project makes one mode. |
format | zip | pdf | png | The file it is. |
url | string | Where to fetch the file. |
expiresAt | string (date-time) or null | When the URL stops working; null while it is kept. |
filename | string | A safe file name from the design's name. |
bytes | integer | Size of the file in bytes. |
warnings | string[] | Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline. |
design | object | The design document. Store it as it is; what you may rely on inside it is on docs.lettrove.com/docs/1/api/design-document. |
{
"exportId": "01J9ZR4C2N8B6V0X3M7K5T9QWE",
"designId": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"revision": 12,
"mode": "email",
"format": "pdf",
"url": "https://files.lettrove.com/embed/…/q3-proposal.pdf",
"expiresAt": null,
"filename": "q3-proposal.pdf",
"bytes": 48213,
"warnings": [],
"design": {}
}
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
400 | request_invalid: The body does not match; the message names the field. |
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project. |
404 | design_not_found: No design with that id is this person's. |
409 | mode_unavailable: A format this mode does not make, such as a popup's PDF.user_erased: This person is being erased. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |
Render a design you hold
POST /embed/v1/render
A design you keep yourself, as the file you send: the same export as everywhere else, by the project's mode. Nothing is stored with Lettrove: an inline image stays inline and is named in warnings.
Limit: 120 requests a minute per key. Operation id: renderDesign.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
doc | object | Yes | The design document, as you hold it. |
designId | string | The id you know this design by. A popup's handle and a form's event carry it. Left out, the document's own id. | |
merge | object | Merge tags to fill. A tag with no value stays as written and is listed in mergeTags. | |
merge.contact | object | The values for {{contact.<field>}} tags, by field. | |
merge.unsubscribeUrl | string | Your unsubscribe link, for {{unsubscribe_url}}. Left out, the tag stays for your sending provider to fill. | |
merge.preferencesUrl | string | Your preferences link, for {{preferences_url}}. Left out, the tag stays. | |
text | object | How the plain-text version is written. | |
text.links | boolean | Each link's address after its text. | |
text.images | boolean | Images' alt text. | |
text.preheader | boolean | The preheader first. |
{
"doc": {},
"designId": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"merge": {
"contact": {
"first_name": "Ada"
},
"unsubscribeUrl": "https://acme.com/u/123"
}
}
200 The rendered design.
| Field | Type | Description |
|---|---|---|
mode | email | page | popup | document | What the design is: email, page, popup or document. A project makes one mode. |
html | string | The whole document, ready to send or serve. |
chunks | object | The HTML in pieces, to place it inside a page of your own. |
chunks.body | string | What goes inside <body>, its scripts taken out. |
chunks.css | string | Every stylesheet the design carries, in order. |
chunks.js | string | Every script it runs, in order; empty for an email. |
chunks.fonts | string[] | The web-font stylesheets it links to, by URL. |
text | string | The plain-text version (the text/plain part), merged the same way. |
subject | string | The subject line, merged. |
preheader | string | The preview text after the subject, merged. |
mergeTags | string[] | Tags still in the HTML, text, subject or preheader, exactly as written: yours to fill before it goes to anyone. |
rendererVersion | integer | Which renderer made it. Goes up when the output changes. |
bytes | integer | Size of html in bytes. |
warnings | string[] | Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline. |
design | object | The design document exported. Store it as it is. |
{
"mode": "email",
"html": "<!doctype html>…",
"chunks": {
"body": "…",
"css": "…",
"js": "",
"fonts": []
},
"text": "Hi Ada, your order has shipped…",
"subject": "Your order has shipped",
"preheader": "It arrives Thursday.",
"mergeTags": [],
"rendererVersion": 7,
"bytes": 38211,
"warnings": [],
"design": {}
}
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
400 | request_invalid: The body does not match; the message names the field.design_invalid: doc is not a design. |
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project. |
409 | mode_unavailable: The design is of another mode than the project's. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |
Erase a person
DELETE /embed/v1/users/{userId}
Erase one person and everything they made: designs, restore points and images, at once, with no grace period. Asking again while it runs is the same request. A user.erased webhook follows when it is done.
Limit: 60 requests a minute per key. Operation id: eraseUser.
| Parameter | In | Type | Description |
|---|---|---|---|
userId | path | string | Your own id for the person: whatever you chose, up to 256 characters. Never a name or an email. |
202 Erasure has started.
| Field | Type | Description |
|---|---|---|
status | "erasing" |
{
"status": "erasing"
}
204 Lettrove never saw this id: there is nothing to erase.
Refusals (body: { "error", "code", "requestId" })
| Status | Code and when |
|---|---|
400 | request_invalid: The user id is not valid. |
401 | key_invalid: The secret key is missing, revoked or not one Lettrove issued. |
403 | project_suspended: The project is suspended.workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project. |
429 | rate_limited: Too many requests for this key; Retry-After says how many seconds to wait. |
503 | service_unavailable: Lettrove cannot do this right now (the embed is not available, or a file format or the queue is briefly down). Nothing was changed; try again. |