Skip to main content

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

FieldTypeRequiredDescription
userobjectYesThe person the editor opens for.
user.idstringYesYour own id for the person: whatever you chose, up to 256 characters. Never a name or an email.
originstringYesThe 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.
ttlintegerSeconds the token lasts, 60 to 900. Default 900.
{
"user": {
"id": "u_123"
},
"origin": "https://app.acme.com",
"ttl": 900
}

200 The token.

FieldTypeDescription
tokenstringHand it to the page, which passes it to the editor. Never store it.
expiresAtstring (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" })

StatusCode and when
400request_invalid: The body does not match; the message names the field.
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_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.
409user_erased: This person is being erased.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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.

ParameterInTypeDescription
userIdpathstringYour own id for the person: whatever you chose, up to 256 characters. Never a name or an email.
limitqueryintegerDesigns per page, 1 to 200. Default 50.
cursorquerystringThe nextCursor of the previous page.

200 A page of designs.

FieldTypeDescription
designsobject[]Newest first. Empty for a person Lettrove has never seen.
designs[].idstringThe id Lettrove gave the design: 26 letters and digits.
designs[].namestringThe name the person gave it.
designs[].modeemail | page | popup | documentWhat the design is: email, page, popup or document. A project makes one mode.
designs[].revisionintegerThe saved revision; only ever goes up.
designs[].createdAtstring (date-time)ISO 8601, UTC.
designs[].updatedAtstring (date-time)ISO 8601, UTC.
nextCursorstring or nullPass 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" })

StatusCode and when
400request_invalid: The user id or the cursor is not valid.
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_suspended: The project is suspended.
workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.
409user_erased: This person is being erased.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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.

ParameterInTypeDescription
userIdpathstringYour own id for the person: whatever you chose, up to 256 characters. Never a name or an email.
designIdpathstringThe id Lettrove gave the design: 26 letters and digits.

200 The design.

FieldTypeDescription
idstringThe id Lettrove gave the design: 26 letters and digits.
namestringThe name the person gave it.
modeemail | page | popup | documentWhat the design is: email, page, popup or document. A project makes one mode.
revisionintegerThe saved revision; only ever goes up.
createdAtstring (date-time)ISO 8601, UTC.
updatedAtstring (date-time)ISO 8601, UTC.
docobjectThe 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" })

StatusCode and when
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_suspended: The project is suspended.
workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.
404design_not_found: No design with that id is this person's. Another person's design is not found either, never refused.
409user_erased: This person is being erased.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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.

ParameterInTypeDescription
idpathstringThe id Lettrove gave the design: 26 letters and digits.

Request body

FieldTypeRequiredDescription
userobjectYesWhose design it is. Another person's design is not found.
user.idstringYesYour own id for the person: whatever you chose, up to 256 characters. Never a name or an email.
mergeobjectMerge tags to fill. A tag with no value stays as written and is listed in mergeTags.
merge.contactobjectThe values for {{contact.<field>}} tags, by field.
merge.unsubscribeUrlstringYour unsubscribe link, for {{unsubscribe_url}}. Left out, the tag stays for your sending provider to fill.
merge.preferencesUrlstringYour preferences link, for {{preferences_url}}. Left out, the tag stays.
textobjectHow the plain-text version is written.
text.linksbooleanEach link's address after its text.
text.imagesbooleanImages' alt text.
text.preheaderbooleanThe preheader first.
formathtml | zip | pdf | pnghtml (the default) answers with the HTML; pdf, png or zip with a file kept for a URL.
fullPagebooleanWith 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

FieldTypeDescription
exportIdstringThe export's record.
designIdstringThe id Lettrove gave the design: 26 letters and digits.
revisionintegerThe saved revision; only ever goes up.
modeemail | page | popup | documentWhat the design is: email, page, popup or document. A project makes one mode.
htmlstringThe whole document, ready to send or serve.
chunksobjectThe HTML in pieces, to place it inside a page of your own.
chunks.bodystringWhat goes inside <body>, its scripts taken out.
chunks.cssstringEvery stylesheet the design carries, in order.
chunks.jsstringEvery script it runs, in order; empty for an email.
chunks.fontsstring[]The web-font stylesheets it links to, by URL.
textstringThe plain-text version (the text/plain part), merged the same way.
subjectstringThe subject line, merged.
preheaderstringThe preview text after the subject, merged.
mergeTagsstring[]Tags still in the HTML, text, subject or preheader, exactly as written: yours to fill before it goes to anyone.
rendererVersionintegerWhich renderer made it. Goes up when the output changes.
bytesintegerSize of html in bytes.
warningsstring[]Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline.
designobjectThe 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

FieldTypeDescription
exportIdstringThe export's record.
designIdstringThe id Lettrove gave the design: 26 letters and digits.
revisionintegerThe saved revision; only ever goes up.
modeemail | page | popup | documentWhat the design is: email, page, popup or document. A project makes one mode.
formatzip | pdf | pngThe file it is.
urlstringWhere to fetch the file.
expiresAtstring (date-time) or nullWhen the URL stops working; null while it is kept.
filenamestringA safe file name from the design's name.
bytesintegerSize of the file in bytes.
warningsstring[]Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline.
designobjectThe 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" })

StatusCode and when
400request_invalid: The body does not match; the message names the field.
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_suspended: The project is suspended.
workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.
404design_not_found: No design with that id is this person's.
409mode_unavailable: A format this mode does not make, such as a popup's PDF.
user_erased: This person is being erased.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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

FieldTypeRequiredDescription
docobjectYesThe design document, as you hold it.
designIdstringThe id you know this design by. A popup's handle and a form's event carry it. Left out, the document's own id.
mergeobjectMerge tags to fill. A tag with no value stays as written and is listed in mergeTags.
merge.contactobjectThe values for {{contact.<field>}} tags, by field.
merge.unsubscribeUrlstringYour unsubscribe link, for {{unsubscribe_url}}. Left out, the tag stays for your sending provider to fill.
merge.preferencesUrlstringYour preferences link, for {{preferences_url}}. Left out, the tag stays.
textobjectHow the plain-text version is written.
text.linksbooleanEach link's address after its text.
text.imagesbooleanImages' alt text.
text.preheaderbooleanThe preheader first.
{
"doc": {},
"designId": "01J9ZQ3W8D2K7M5T1V4XG6HB0R",
"merge": {
"contact": {
"first_name": "Ada"
},
"unsubscribeUrl": "https://acme.com/u/123"
}
}

200 The rendered design.

FieldTypeDescription
modeemail | page | popup | documentWhat the design is: email, page, popup or document. A project makes one mode.
htmlstringThe whole document, ready to send or serve.
chunksobjectThe HTML in pieces, to place it inside a page of your own.
chunks.bodystringWhat goes inside <body>, its scripts taken out.
chunks.cssstringEvery stylesheet the design carries, in order.
chunks.jsstringEvery script it runs, in order; empty for an email.
chunks.fontsstring[]The web-font stylesheets it links to, by URL.
textstringThe plain-text version (the text/plain part), merged the same way.
subjectstringThe subject line, merged.
preheaderstringThe preview text after the subject, merged.
mergeTagsstring[]Tags still in the HTML, text, subject or preheader, exactly as written: yours to fill before it goes to anyone.
rendererVersionintegerWhich renderer made it. Goes up when the output changes.
bytesintegerSize of html in bytes.
warningsstring[]Things worth knowing, in words: an email past Gmail's clipping size, an inline image left inline.
designobjectThe 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" })

StatusCode and when
400request_invalid: The body does not match; the message names the field.
design_invalid: doc is not a design.
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_suspended: The project is suspended.
workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.
409mode_unavailable: The design is of another mode than the project's.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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.

ParameterInTypeDescription
userIdpathstringYour own id for the person: whatever you chose, up to 256 characters. Never a name or an email.

202 Erasure has started.

FieldTypeDescription
status"erasing"
{
"status": "erasing"
}

204 Lettrove never saw this id: there is nothing to erase.

Refusals (body: { "error", "code", "requestId" })

StatusCode and when
400request_invalid: The user id is not valid.
401key_invalid: The secret key is missing, revoked or not one Lettrove issued.
403project_suspended: The project is suspended.
workspace_suspended: Lettrove has suspended the embed for the workspace that owns the project.
429rate_limited: Too many requests for this key; Retry-After says how many seconds to wait.
503service_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.