Emails
Read and edit simple-editor emails block by block: content, design settings, HTML previews, versions and templates.
Emails built with the simple editor are made up of ordered content blocks and a set of styles shared across the whole email. These endpoints read and change both.
Email IDs come from the endpoints that own the email: a campaign's emails array, or an automation step's email_id. Emails built with other editors are not available here.
Changes made by someone else
Someone can have the email open in the editor while you change it through the API. To keep your change from silently replacing theirs, every write that changes the email must say which version of the email it is based on. Saving a version and saving the email as a template do not change it, so they do not take an etag.
- Read the email, a block or its HTML. The response body has an
etag, for example"42". - Send that value as
etagin the JSON body of your next write. This includesDELETE. - Use the
etagthat each write returns for the write after it. Do not reuse an older one.
A write that takes an etag and is sent without one gets 422 and nothing is changed. If the email changed after you read it, the write gets 409 with code set to version_conflict, and nothing is changed. That response has the current etag, but your copy of the email is out of date. Fetch the email again, then decide whether to retry.
Response code: 409 Conflict{
"message": "The email changed since you read it. Fetch it again, then retry with its new etag.",
"code": "version_conflict",
"etag": "43"
}Other 409 responses carry only a message and no code, so fetching a new etag does not help with them:
| Message | When |
|---|---|
This email can only be edited while its campaign is a draft. | The email's campaign is no longer a draft. |
The email is being changed by another request. Fetch it again and retry. | Another write to the same email is still being processed. |
A request with this Idempotency-Key is still in progress. | A request with the same Idempotency-Key is still running. |
The etag covers the whole email, not one block. Writes sent in parallel with the same etag give one success, and the others get 409. Send writes to one email one after another.
In /batch, put etag in each request's body.
Blocks
blocks lists the email's content in the order it appears. Each block has an id, a type, and an editor_content object. A block that contains other blocks carries them in a children array.
type is one of blockquote, bulleted_list, button, code, divider, heading, image, numbered_list, paragraph, social_icon, title or video. The list grows as the editor gains block types.
Editor content
editor_content holds the editor's document for that block, forwarded exactly as it is stored, so its keys are camelCase. Everything around it uses snake_case, as elsewhere in the API. The footer's company_name, address and info_text are editor documents too, so each is wrapped the same way.
Social icons
A social_icon block is a row of icons rather than text, so its editor_content carries attributes and no document. items holds the icons in the order they are shown. Each item carries a uuid that identifies it within the block, the network it links to as type, the url it points at, a display label, and a linkId used for click tracking. shape, color, size and alignment apply to the row as a whole, and targetBlank says whether the links open in a new tab. The alignment is left, center or right.
shape, color and the network name are each a segment of the icon image's URL, so all three come from fixed vocabularies. The shape is default (Default), round (Circle), round_stroke (Outlined Circle), square (Square), rounded_corners (Rounded Square) or rounded_stroke (Outlined Square), with the editor's own name for each in brackets, since several of them do not read the way the value does. The colour is black, color, grey or white, which the editor labels the same way. color means the network's own brand colours rather than a colour you pick.
The network is one of amazon, apple_podcasts, audible, bandcamp, behance, bluesky, bookbub, clubhouse, deviantart, discord, dribbble, email, facebook, flickr, foursquare, gettr, github, goodreads, google_podcasts, imdb, instagram, linkedin, location, mastodon, medium, mewe, odnoklassniki, open_sea, overcast, patreon, periscope, pinterest, quora, reddit, riot, rss, slack, snapchat, soundcloud, spotify, steam, strava, telegram, threads, tiktok, tumblr, twitch, viber, vimeo, vine, website, whatsapp, wordpress, x, xing or youtube. The editor offers each network once per block.
size is the icon's width and height in pixels. The editor writes an even number between 16 and 40, which is the range its slider offers; the stored shape itself takes any whole number above zero. The gap between icons is half the size, rounded.
An item with no url is returned here but left out of the email that is sent, and a block whose items all lack one renders nothing at all.
Get an email
If you want to get an email's content blocks, in the order they appear, along with the styles applied across the whole email, use this GET request
GET https://connect.mailerlite.com/api/emails/{email_id}Only emails built with the simple editor can be read here; any other email returns 422.
Settings
settings covers the styles applied across the email — brand, typography, button, layout and footer.
Styles are returned as they apply to the email rather than as raw stored values, so brand always contains the colours and font in use. The footer's unsubscribe_text and preferences_text fall back to your account's own wording, then to the template the email was built from. Whether a preferences link is shown is show_preferences, separately from its wording.
| Field | Description |
|---|---|
| id | The email ID. |
| type | The builder the email was made with. Always builder_inline_email here, since only simple-editor emails are returned. |
| emailable_type | What the email belongs to: campaigns or automations. |
| emailable_id | The ID of the campaign or automation the email belongs to. |
| settings | The styles applied across the whole email. |
| settings.footer.preferences_text | Left out when the preference centre is not on your plan. |
| blocks | The email's content blocks, in the order they appear. |
| blocks.*.children | Blocks nested inside this one. Left out when there are none. |
| etag | The email's current version. Send it as etag on your next write. |
Response
Response code: 200 OK{
"data": {
"id": "167329348543236005",
"type": "builder_inline_email",
"emailable_type": "campaigns",
"emailable_id": "167329348523312548",
"settings": {
"brand": {
"mode": "account",
"font_family": "Inter, sans-serif",
"background_color": "#ffffff",
"text_color": "#515856",
"border_color": "#eaeced"
},
"layout": {
"width_mode": "default",
"alignment": "center"
},
"typography": {
"link": { "color": "#2cb191" },
"heading": {
"font_family": "Inter, sans-serif",
"font_weight": "700",
"line_height": "137%",
"letter_spacing": "0",
"h1": { "color": "#111111", "size": "32" },
"h2": { "color": "#111111", "size": "24" },
"h3": { "color": "#111111", "size": "20" }
},
"paragraph": {
"font_family": "Inter, sans-serif",
"font_weight": "400",
"line_height": "165%",
"letter_spacing": "0",
"p1": { "color": "#333333", "size": "16" }
}
},
"button": {
"font_family": "Inter, sans-serif",
"font_weight": "600",
"letter_spacing": "0",
"background_color": "#2cb191",
"text_color": "#ffffff",
"border_radius": "8"
},
"footer": {
"show_preferences": false,
"company_name": {
"editor_content": { "content": [{ "type": "text", "text": "Acme Inc." }] }
},
"address": {
"editor_content": { "content": [{ "type": "text", "text": "1 Main Street, Springfield, United States" }] }
},
"info_text": {
"editor_content": { "content": [{ "type": "text", "text": "You received this email because you signed up on our website." }] }
},
"unsubscribe_text": "Unsubscribe",
"preferences_text": "Update your preferences",
"social_links": [
{ "url": "https://www.instagram.com/acme", "type": "instagram", "label": "Instagram", "link_id": 1001 }
]
}
},
"blocks": [
{
"id": "55",
"type": "heading",
"editor_content": {
"attrs": { "level": 1, "textAlign": "left" },
"content": [{ "type": "text", "text": "Our October update" }]
}
},
{
"id": "56",
"type": "image",
"editor_content": {
"attrs": { "src": "https://storage.mlcdn.com/account_image/123/october.png", "alt": "October highlights" }
}
},
{
"id": "57",
"type": "paragraph",
"editor_content": {
"attrs": { "textAlign": "left" },
"content": [{ "type": "text", "text": "Here is what shipped this month." }]
}
},
{
"id": "58",
"type": "button",
"editor_content": {
"attrs": { "url": "https://example.com/october", "linkId": 1002 },
"content": [{ "type": "text", "text": "Read the update" }]
}
}
]
},
"etag": "42"
}Error
When the email was not built with the simple editor
Response code: 422 Unprocessable Entity{
"message": "This email was not built with the simple editor."
}When providing an invalid email id
Response code: 404 Not FoundGet an email's HTML
If you want to get an email's HTML, rendered from its content blocks and settings, use this GET request
GET https://connect.mailerlite.com/api/emails/{email_id}/htmlGET /emails/{email_id}/html returns the email's HTML as a single string.
The HTML always reflects the email's current content and settings, including changes made through the API.
This HTML is a preview, not a sendable email. Personalisation comes back as literal tokens ({$name}, {$unsubscribe}), and links carry linkid markers instead of tracking URLs. The send pipeline substitutes both, so do not deliver this HTML yourself.
Response
Response code: 200 OK{
"data": {
"html": "<!doctype html>..."
},
"etag": "42"
}Error
An email that cannot be rendered returns 503. This is temporary — retry the request.
Response code: 503 Service Unavailable{
"message": "This email could not be rendered. Please try again."
}Get a block
If you want to get one of the email's content blocks, with its nested children if it has any, use this GET request
GET https://connect.mailerlite.com/api/emails/{email_id}/blocks/{block_id}{block_id} is the block ID. Use the id of any block in the email, including blocks nested in children. Its id, type and editor_content match the block of the same id in the email's blocks array exactly.
placement gives its position among the blocks in the same parent: after is the ID of the block before it, and before is the ID of the block after it. Each is null at the start or end of that list. To move or insert a block next to one of these neighbours, send its ID as placement.block_id with a placement.position; after and before are not accepted as placement themselves.
Response
Response code: 200 OK{
"data": {
"id": "57",
"type": "paragraph",
"editor_content": {
"attrs": { "textAlign": "left" },
"content": [{ "type": "text", "text": "Here is what shipped this month." }]
},
"placement": {
"after": "56",
"before": "58"
}
},
"etag": "42"
}Error
When providing a block id this email does not have
Response code: 404 Not FoundInsert a block
If you want to add a new block to the email, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/blocksPOST /emails/{email_id}/blocks adds a new block to the email and returns it in the same shape as GET /emails/{email_id}/blocks/{block_id}, including its resolved placement. A block cannot be inserted inside another block, so placement.block_id must be a top-level block.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| type | string | yes | Must be one of the following: blockquote, bulleted_list, button, code, divider, heading, image, numbered_list, paragraph, social_icon, video. Any other type returns 422 |
| editor_content | object | yes | The editor's document for the block. May be empty for blocks with no data, like a divider. |
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
| placement | object | no | Where the block goes, relative to its siblings. Defaults to { "position": "end" }. An empty placement is refused |
| placement.position | string | yes, if placement is sent | start or end of the sibling list, or before or after the block given in placement.block_id. |
| placement.block_id | integer | only if position is before or after | The sibling block to place against. A block_id that is not a sibling of the newly inserted block — including a missing, foreign, or soft-deleted block — returns 422 |
{
"etag": "42",
"type": "paragraph",
"editor_content": {
"content": [
{ "type": "text", "text": "Read the " },
{ "type": "text", "text": "full story", "marks": [{ "type": "bold" }] },
{ "type": "text", "text": " on our blog", "marks": [
{ "type": "link", "attrs": { "href": "https://example.com/blog" } }
] }
]
},
"placement": { "position": "after", "block_id": 57 }
}A button carries its destination in attrs and its label in content, as text like any other block:
{
"etag": "42",
"type": "button",
"editor_content": {
"attrs": { "url": "https://example.com/shop" },
"content": [{ "type": "text", "text": "Shop now" }]
}
}A bulleted_list or numbered_list holds listItem nodes. Each item holds one nodeParagraph with the text, then optionally a nested bulletList or orderedList. The editor's node names are used here, so write nodeParagraph, not paragraph:
{
"etag": "42",
"type": "bulleted_list",
"editor_content": {
"attrs": {},
"content": [
{ "type": "listItem", "content": [
{ "type": "nodeParagraph", "content": [{ "type": "text", "text": "Free shipping" }] }
] },
{ "type": "listItem", "content": [
{ "type": "nodeParagraph", "content": [{ "type": "text", "text": "30-day returns" }] },
{ "type": "bulletList", "content": [
{ "type": "listItem", "content": [
{ "type": "nodeParagraph", "content": [{ "type": "text", "text": "No questions asked" }] }
] }
] }
] }
]
}
}editor_content is required but may be an empty object — a divider, for example, carries no meaningful data:
{ "etag": "42", "type": "divider", "editor_content": {} }Marks and nodes
A node or mark type that the editor does not know returns 422 with the key of that type, for example editor_content.content[0].content[0].type.
Text can carry the marks bold, italic, underline, strike, code, link, textStyle, subscript and superscript. The footer's company_name, address and info_text take the same marks except code. Any other mark returns 422.
Link tracking
No link tracking management is needed. Every link in editor_content, whether a link mark or a button's url, is given its own tracking ID when the block is created, and the response shows it. A button with an empty url gets none. linkId is assigned automatically; any value in the request is replaced.
Response
Response code: 201 Created{
"data": {
"id": "59",
"type": "button",
"editor_content": {
"attrs": { "url": "https://example.com/shop", "linkId": 1003 },
"content": [{ "type": "text", "text": "Shop now" }]
},
"placement": {
"after": "58",
"before": null
}
},
"etag": "43"
}Error
A value inside editor_content that is not allowed in email content, such as a javascript: URL, returns 422 naming the key, and no block is created
Response code: 422 Unprocessable Entity{
"message": "This value is not allowed in email content and was rejected.",
"errors": {
"editor_content.attrs.url": ["This value is not allowed in email content and was rejected."]
}
}version_conflict: The email changed since you read it. Fetch it again, then retry with its new etag. See Changes made by someone else.
Response code: 409 ConflictValidate a block
If you want to check a block against the same rules as inserting it, without saving anything, use this POST request
POST https://connect.mailerlite.com/api/emails/blocks/validateSend exactly what you would send to POST /emails/{email_id}/blocks. The check is not tied to an email, so it does not check the etag, and it cannot tell whether placement.block_id is in the email you go on to insert into.
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| type | string | yes | The block type to check editor_content against. An unknown type returns valid: false, not a 422 |
| editor_content | object | yes | The editor's document for the block, exactly as you would send it to insert the block |
| placement | object | no | Where the block would go, relative to its siblings. Checked as the insert would check it, except that it cannot tell whether block_id is in the email you go on to insert into |
| placement.position | string | yes, if placement is sent | start or end of the sibling list, or before or after the block given in placement.block_id. |
| placement.block_id | integer | only if position is before or after | The sibling block to place against |
{ "type": "paragraph", "editor_content": { "attrs": { "textAlign": "sideways" } } }Response
Invalid content is a successful check, so the response is 200 either way. errors is keyed and worded exactly as inserting the block would report it, so it can be fixed before the insert is sent. schema identifies the version of the block rules the check ran against.
Response code: 200 OK{
"valid": false,
"errors": { "editor_content.attrs.textAlign": ["..."] },
"schema": { "version": "2.73.2", "hash": "..." }
}Error
A request missing type or editor_content, or whose editor_content is a string or number, returns 422. A list is checked like any other content and comes back valid: false.
Response code: 422 Unprocessable EntityUpdate a block
If you want to replace what is inside a block, use this PATCH request
PATCH https://connect.mailerlite.com/api/emails/{email_id}/blocks/{block_id}PATCH /emails/{email_id}/blocks/{block_id} replaces what is inside a block and returns it in the same shape as GET /emails/{email_id}/blocks/{block_id}.
editor_content is replaced whole, not merged — it is a document tree, and there is no key to match its nodes on. Read the block first, change what you want, and send the result back. Anything you leave out is gone.
A block's type cannot be changed, since each type stores a different shape of content: delete the block and insert one of the type you want. Position is not changed here either — use the move endpoint. Sending either with a value is a 422, even when type is the block's current type. Any other field is ignored.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| editor_content | object | yes | The block's full content document, attrs included, which replaces the stored one rather than merging into it. {} clears the block |
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
{
"etag": "43",
"editor_content": {
"attrs": { "textAlign": "center" },
"content": [{ "type": "text", "text": "Hello" }]
}
}Content that does not fit the block's type is refused with a 422 naming each offending key, the same verdict you would get from validating the block on its own. Where a value is allowed to be null, null means "unset" and the email's own styles decide how it renders; where it is not, it is refused.
A link keeps the tracking ID it already has in this block, so a link you read and send back is still the same link in your reports. Every other link is given a new ID: one with no ID, and one carrying an ID the block does not already hold, whatever its value. A button or image with an empty url gets none. The response shows the IDs assigned.
Response
Response code: 200 OK{
"data": {
"id": "57",
"type": "paragraph",
"editor_content": {
"attrs": { "textAlign": "center" },
"content": [{ "type": "text", "text": "Hello" }]
},
"placement": {
"after": "56",
"before": "58"
}
},
"etag": "44"
}Error
When trying to change a block's type or position
Response code: 422 Unprocessable Entityversion_conflict: The email changed since you read it. Fetch it again, then retry with its new etag.
Response code: 409 ConflictMove a block
If you want to move a block among its siblings, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/blocks/{block_id}/movePOST /emails/{email_id}/blocks/{block_id}/move moves a block within the email and returns it in the same shape as GET /emails/{email_id}/blocks/{block_id}, with placement reflecting its new position. The block keeps its parent, so it cannot move into or out of another block.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
| placement | object | yes | Where the block goes, relative to its siblings. An empty placement is refused |
| placement.position | string | yes | start or end of the sibling list, or before or after the block given in placement.block_id. |
| placement.block_id | integer | only if position is before or after | The sibling block to place against. Required when placement.position is before or after. |
{
"etag": "44",
"placement": { "position": "after", "block_id": 57 }
}block_id names the sibling to move next to and is required for before and after; it is ignored for start and end, where any value you send is discarded without validation. Moving a block does not change its nesting.
Moving a block to where it already is succeeds unchanged and returns 200 with the block.
Response
Response code: 200 OK{
"data": {
"id": "56",
"type": "image",
"editor_content": {
"attrs": { "src": "https://storage.mlcdn.com/account_image/123/october.png", "alt": "October highlights" }
},
"placement": {
"after": "57",
"before": "58"
}
},
"etag": "45"
}Error
A block_id that is not a sibling of the block being moved — including a missing, foreign, or soft-deleted block, or the block being moved itself — returns 422.
Response code: 422 Unprocessable EntityDelete a block
If you want to remove a block from the email, use this DELETE request
DELETE https://connect.mailerlite.com/api/emails/{email_id}/blocks/{block_id}DELETE /emails/{email_id}/blocks/{block_id} removes a block from the email and returns 200 with the email's new etag. Blocks nested inside the one you delete are removed along with it.
The blocks around it keep their order, so the block that was before the deleted one is now directly before the block that was after it. Deleting the last remaining block leaves the email with no content blocks; its settings are untouched.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
{ "etag": "45" }Response
Response code: 200 OK{
"etag": "46"
}Error
Deleting a block that is already gone returns 404, the same as any block ID this email does not have.
Response code: 404 Not FoundUpdate the design settings
If you want to change the styles applied across the email, use this PATCH request
PATCH https://connect.mailerlite.com/api/emails/{email_id}/settingsPATCH /emails/{email_id}/settings changes the styles applied across the email. Send the settings object from GET /emails/{email_id}, with only the keys you want to change:
{
"etag": "46",
"brand": { "mode": "custom", "background_color": "#f4f4f4" }
}data comes back in exactly the shape settings has in GET /emails/{email_id}, resolved again after the write, so you can compare what you sent against what stuck.
Only the keys you send are changed; everything else is left alone. The one exception is footer.social_links: it is an ordered list, so sending it replaces the whole list. Send every link you want to keep, in the order you want them.
Anything outside the writable set is a 422 naming the key rather than being quietly ignored.
An email on a campaign that is no longer a draft returns 409, and one that does not belong to a campaign, such as an automation email, returns 422.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
| layout | object | no | The email's width mode (default or full) and alignment (left or center), in the shape of settings.layout from GET /emails/{email_id} |
| typography | object | no | Heading, paragraph and link styles, in the shape of settings.typography from GET /emails/{email_id} |
| button | object | no | Button styles, in the shape of settings.button from GET /emails/{email_id} |
| brand | object | no | The email's palette: its font and colours. See Brand for how these values interact with mode |
| brand.mode | string | no | Where the palette comes from. Sending any other brand key, style included, without mode switches the email to custom. account and template set the palette themselves: a colour or font sent with them must match that palette, or the request returns 422. One of account (the email follows the account's brand styles), template (the email keeps the styles of the template it was created from) or custom (the styles have been edited on this email). |
| brand.style | string | no | Write-only. A preset identifier from GET /brand-styles, such as preset-retro, whose palette is applied to the email. The email then reads back as mode: custom |
| brand.font_family | string | no | The brand font stack, such as Inter, sans-serif. Use only letters, digits, spaces, commas and hyphens; quotes are removed. It sets the email's base font, which in practice shows in the footer — headings, paragraphs and buttons carry their own font in typography and button. Send an empty string to go back to your account's brand font |
| brand.background_color | string | no | The email's background colour, as a hex colour such as #1a2b3c, or transparent. A colour that makes the unsubscribe link unreadable is refused. Send an empty string to go back to your account's brand colour |
| brand.text_color | string | no | The email's base text colour, used for footer text and for text with no colour of its own. Paragraph text uses typography.paragraph.p1.color. Send an empty string to go back to your account's brand colour |
| brand.border_color | string | no | The colour of the footer's top border, and of dividers and quote borders that do not set their own colour. Send an empty string to go back to your account's brand colour |
| footer | object | no | The footer shown at the end of the email |
| footer.show_preferences | boolean | no | Whether the footer shows a link to the preference centre. true needs the preference centre on your plan; false is always accepted |
| footer.unsubscribe_text | string | no | The wording of the unsubscribe link, as plain text. Send an empty string to go back to your account's wording |
| footer.preferences_text | string | no | The wording of the preference centre link, as plain text. Send an empty string to go back to your account's wording. Needs the preference centre on your plan; without it, sending this key at all returns 422 |
| footer.company_name | object | no | Your company name as shown in the footer. |
| footer.company_name.editor_content | object | no | The editor document holding the company name. Send {} or {"content": []} to clear it, so it reads back as your account's company name. |
| footer.address | object | no | Your postal address as shown in the footer. |
| footer.address.editor_content | object | no | The editor document holding the address. Send {} or {"content": []} to clear it, so it reads back as your account's postal address, or a placeholder if your account has none. |
| footer.info_text | object | no | Any extra footer wording, such as why the recipient is hearing from you. |
| footer.info_text.editor_content | object | no | The editor document holding the extra wording. Send {} or {"content": []} to clear it, so it reads back as your account's unsubscribe disclaimer, or stays empty if your account has none. |
| footer.social_links | array | no | The social icons shown in the footer. This is an ordered list, so sending it replaces the whole list. Every entry needs url, type and label, or the request returns 422. An entry with an empty url is not shown |
| footer.social_links.*.url | string | yes | Where the icon links to |
| footer.social_links.*.type | string | yes | The network the icon is for, such as facebook. Use a network from the list under Social icons, or the icon does not load. The value is stored in lowercase |
| footer.social_links.*.label | string | yes | The icon's alt text. If it is empty, type is used |
| footer.social_links.*.link_id | integer | no | The link's ID in this email, which click tracking matches on. Send back the one you read. Without it, the link gets a new ID, and later clicks are not counted with its earlier clicks |
Value formats
Style values must use the format and the range the editor offers. Any other value is a 422 that names the field.
- Colours: a hex colour, such as
#1a2b3cor#abc, ortransparent. A brand colour can also be empty. Other colours cannot. - Sizes and
button.border_radius: a whole number without a unit, sent as a string, such as"16"."16px"and"16.5"are refused. Heading sizes go from"10"to"96",typography.paragraph.p1.sizefrom"10"to"32", andbutton.border_radiusfrom"0"to"40". - Letter spacing:
"-0.05","-0.025","0","0.025"or"0.05". - Line height:
"100%","137%","150%","165%"or"200%". - Font weight:
"300","400","500","600"or"700", sent as a string. - Font family: letters, digits, spaces, commas and hyphens, such as
Inter, sans-serif. Quotes are removed before the value is checked.
Send every style value as a JSON string. A JSON number, such as 16 instead of "16", is a 422.
Everything you send is stored
The read returns values as they apply, including ones inherited from your account; the write stores what you send. Sending an inherited value back pins it to this email, so it stops following your account and "Force-update company details" no longer reaches it. Send only the keys you mean to change.
To go back to inheriting, send the value blank. An empty string on a brand colour, or an empty editor document on a footer text, means "unset" — the value is stored empty and reads back as your account's, where your account has one. A cleared address reads back as a placeholder if your account has no company details, and a cleared info_text stays empty if your account has no unsubscribe disclaimer:
{ "etag": "46", "footer": { "info_text": { "editor_content": { "content": [] } } } }An empty editor_content object — {} — means the same thing.
Brand
brand.mode picks where the palette comes from:
account— follow your account's brand styles. The email's own colours are cleared.template— go back to the palette the email's template was authored with.custom— this email's own colours.
Sending any other brand key, style included, without mode switches the email to custom.
brand.style is write-only: send a preset identifier from GET /brand-styles, such as preset-retro, to apply that preset's palette. It reads back as mode: custom, and any colour sent alongside it wins. For your account's or the template's palette use brand.mode; for the editor's default palette, send its font_family, background_color, text_color and border_color from GET /brand-styles with mode: custom.
mode: account and mode: template set the palette themselves. A colour or font sent with them must match the palette that mode sets, as GET /emails/{email_id} returns it, so you can send the read back unchanged. A blank value is accepted wherever that palette follows your account: always with mode: account, and with mode: template when the template does not set that value. Every value that differs is named in one 422, and brand.style is always a 422.
Colours are lowercased and font stacks lose their quotes when stored, so a palette taken from GET /brand-styles reads back identical.
Social links and link IDs
Each entry in footer.social_links carries a link_id that click reports are keyed on. Send it back with the link. An entry that arrives without one is treated as a brand new link and is given a fresh ID, and the click history on the old ID stops matching.
If two entries arrive holding the same link_id, or an ID already used elsewhere in the email, the later one is moved to a new ID rather than the first being disturbed. An entry with an empty url and no ID is stored without one.
A social link needs url, type and label. A stored link that is missing one of these reads back without it, so sending that entry straight back is a 422 naming the missing field — fill it in before sending it on.
Link labels
footer.unsubscribe_text and footer.preferences_text are plain text in both directions. The editor can style these links, for example in bold; the read returns only their text. Setting either one to new text replaces that styling with your plain text, while sending the current text back leaves the styling in place.
Preferences link
footer.show_preferences: true and any footer.preferences_text need the preference centre on your plan; without it both are a 422. show_preferences: false is always accepted, since that is what the read returns.
Response
Response code: 200 OK{
"data": {
"brand": {
"mode": "custom",
"font_family": "Inter, sans-serif",
"background_color": "#f4f4f4",
"text_color": "#515856",
"border_color": "#eaeced"
},
"layout": { "width_mode": "default", "alignment": "center" },
"typography": { "...": "..." },
"button": { "...": "..." },
"footer": { "...": "..." }
},
"etag": "47"
}Rejected attributes
A value that fails the content sanitiser does not fail the request. The write succeeds, the unsafe value is dropped or replaced, and the response carries a rejected object alongside data:
{
"data": { "...": "..." },
"rejected": {
"message": "Some attributes were rejected.",
"keys": ["footer.social_links[0].url"]
},
"etag": "47"
}rejected.keys covers the whole footer after your changes are merged in, so it can name an unsafe value that was already stored, not just one you sent.
Writing a block reports dropped attributes the same way, but only those. An attribute or mobile override the block's type does not declare is removed rather than stored, the write still succeeds, and rejected.keys names it. Only image and video blocks declare a mobile override, hidden. A value that is not allowed in email content is still a 422, as under Insert a block:
{
"data": { "...": "..." },
"rejected": {
"message": "Some attributes were rejected.",
"keys": ["editor_content.attrs.onclick"]
},
"etag": "47"
}POST /emails/blocks/validate reports the same rejected object, so a block can be checked before it is written. A check that fails leaves it out: nothing would be stored, so nothing is dropped.
Error
An email that was not built with the simple editor, or that does not belong to a campaign, returns a 422 with only message. Validation failures also carry an errors object keyed by field.
Response code: 422 Unprocessable Entity{
"message": "The given data was invalid.",
"errors": {
"button.border_radius": ["..."]
}
}Paths and field names inside errors use the API's own names. A required setting this API does not expose is named as the editor stores it, since there is no API name to give it.
The whole design is checked after your changes are merged in, so errors can also name a value that was already stored in a format that is no longer accepted.
When the campaign is no longer a draft
Response code: 409 ConflictValidate design settings
If you want to check a change to an email's design settings against the same rules as updating them, without saving anything, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/settings/validatePOST /emails/{email_id}/settings/validate checks a change to the design settings against the same rules as PATCH /emails/{email_id}/settings, without saving anything. Send exactly what you would send to the update. The etag is ignored here, so a stale etag is not detected.
{ "layout": { "width": 700 } }The change is checked against the email's settings as they are stored now, so errors can name a value that was already stored in a format that is no longer accepted.
Response
Invalid settings are a successful check, so the response is 200 either way. errors is keyed and worded exactly as the update would report it:
Response code: 200 OK{
"valid": false,
"errors": { "layout.width": ["The layout.width field cannot be changed."] },
"schema": { "version": "2.73.2", "hash": "..." }
}When the update would succeed but strip a value as unsafe, the check still passes, and rejected names the value in the same shape the update returns:
{
"valid": true,
"errors": {},
"schema": { "version": "2.73.2", "hash": "..." },
"rejected": {
"message": "Some attributes were rejected.",
"keys": ["footer.social_links[0].url"]
}
}Error
An email the update would refuse — not built with the simple editor, past draft, or not in a campaign — is refused here in the same way.
Save an email as a template
If you want to copy the email's content and styles into a new template, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/templatesThe template shows in your saved templates, and you can start a campaign from it by sending its ID as emails.*.template_id to POST /campaigns. The email itself is left as it is. Only a campaign's email can be saved this way — an automation's email is refused.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| name | string | yes | The name for the new template, as it will appear in your template list. Up to 255 characters. Names that contain URLs, template syntax such as {{, or HTML are refused |
{ "name": "October newsletter" }Response
Response code: 201 Created{
"data": {
"id": "4321",
"name": "October newsletter",
"type": "email",
"created_at": "2026-10-14 10:21:07"
}
}Error
Response code: 422 Unprocessable Entity{
"message": "The name field is required.",
"errors": {
"name": ["The name field is required."]
}
}Versions
A version is a saved copy of an email's content and settings that you can go back to.
Versions are saved when someone saves the email in the editor, and when you call the endpoint below — editing through the API does not create one on its own, so an email built entirely through the API has no versions until you save one.
Saving and restoring write to the email, so they need an email that belongs to a campaign and is still a draft: an email that belongs to an automation instead returns 422, and one whose campaign has left draft returns 409. Listing versions only reads, so it works for either.
List versions
If you want to list an email's saved versions, newest first, use this GET request
GET https://connect.mailerlite.com/api/emails/{email_id}/versionsEach version has an id, the created_at it was saved at, and current, which marks the version the email is sitting on.
Request parameters
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| limit | integer | no | How many versions to return per page. Defaults to 25, up to 100 |
| page | integer | no | Which page to return, starting at 1 |
Response
Response code: 200 OK{
"data": [
{
"id": "902",
"created_at": "2026-10-14 10:20:31",
"current": true
},
{
"id": "901",
"created_at": "2026-10-13 16:02:11",
"current": false
}
],
"links": {
"first": "https://connect.mailerlite.com/api/emails/167329348543236005/versions?page=1",
"last": "https://connect.mailerlite.com/api/emails/167329348543236005/versions?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [
{ "url": null, "label": "« Previous", "page": null, "active": false },
{ "url": "https://connect.mailerlite.com/api/emails/167329348543236005/versions?page=1", "label": "1", "page": 1, "active": true },
{ "url": null, "label": "Next »", "page": null, "active": false }
],
"path": "https://connect.mailerlite.com/api/emails/167329348543236005/versions",
"per_page": 25,
"to": 2,
"total": 2
}
}Save a version
If you want to save the email as it currently stands as a version that can be restored later, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/versionsRequest headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Response
POST /emails/{email_id}/versions saves the email as it currently stands and returns the new version with 201. If nothing has changed since the last version, no new version is saved and that one is returned with 200 instead.
Response code: 201 Created{
"data": {
"id": "903",
"created_at": "2026-10-14 11:45:02",
"current": true
}
}Restore a version
If you want to put the email back to one of its saved versions, use this POST request
POST https://connect.mailerlite.com/api/emails/{email_id}/versions/{version_id}/restorePOST /emails/{email_id}/versions/{version_id}/restore puts the email back to a version and returns the email in the same shape as GET /emails/{email_id}. Blocks come back under the IDs they had when the version was saved, so a block you deleted returns under its original ID. The state you are replacing is saved as a version first, so a restore can itself be undone by restoring the version it created.
Request headers
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| Idempotency-Key | string | no | A unique value of up to 255 printable ASCII characters. A repeat of the same request with the same key within 24 hours returns the original response. See Idempotency |
Request body
| Parameter | Type | Required | Limitations |
|---|---|---|---|
| etag | string | yes | The etag that your last read or write of this email returned. If the email changed since, the write is refused with 409 |
{ "etag": "47" }Response
Response code: 200 OK{
"data": {
"id": "167329348543236005",
"type": "builder_inline_email",
"emailable_type": "campaigns",
"emailable_id": "167329348523312548",
"settings": { "...": "..." },
"blocks": [ { "...": "..." } ]
},
"etag": "48"
}Error
A version_id this email does not have returns 404, the same as one that does not exist.
Response code: 404 Not FoundWhen the version was saved in a shape the current block rules no longer accept
Response code: 422 Unprocessable Entity{
"message": "This version can no longer be restored."
}