Create a transactional email
This creates a transactional email and a related draft email message in one request. Only aname value is required.
Save the returned
draftEmailMessageContentRevisionId. Pass it as
expectedRevisionId when updating the draft email message to avoid 409 Conflict errors caused by stale revisions.const response = await fetch("https://app.loops.so/api/v1/transactional-emails", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Password reset",
}),
});
const data = await response.json();
const transactionalId = data.id;
const draftEmailMessageId = data.draftEmailMessageId;
const draftEmailMessageContentRevisionId =
data.draftEmailMessageContentRevisionId;
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const data = await loops.createTransactionalEmail({
name: "Password reset",
});
const transactionalId = data.id;
const draftEmailMessageId = data.draftEmailMessageId;
const draftEmailMessageContentRevisionId =
data.draftEmailMessageContentRevisionId;
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$result = $loops->transactional->create(name: 'Password reset');
$transactional_id = $result['id'];
$draft_email_message_id = $result['draftEmailMessageId'];
$draft_email_message_content_revision_id =
$result['draftEmailMessageContentRevisionId'];
response = LoopsSdk::Transactional.create(
name: "Password reset",
)
transactional_id = response["id"]
draft_email_message_id = response["draftEmailMessageId"]
draft_email_message_content_revision_id =
response["draftEmailMessageContentRevisionId"]
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional-emails",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"name": "Password reset",
},
)
data = response.json()
transactional_id = data["id"]
draft_email_message_id = data["draftEmailMessageId"]
draft_email_message_content_revision_id = data[
"draftEmailMessageContentRevisionId"
]
Query themes and components for your LMX
You can fetch your available themes and reusable components before building thelmx payload.
List themes API reference CLI referenceList components API reference CLI reference
loops themes list -o json
loops components list -o json
const [themesResponse, componentsResponse] = await Promise.all([
fetch("https://app.loops.so/api/v1/themes?perPage=20", {
method: "GET",
headers: {
"Authorization": "Bearer <your-api-key>",
},
}),
fetch("https://app.loops.so/api/v1/components?perPage=20", {
method: "GET",
headers: {
"Authorization": "Bearer <your-api-key>",
},
}),
]);
const themes = await themesResponse.json();
const components = await componentsResponse.json();
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const [themes, components] = await Promise.all([
loops.listThemes({ perPage: 20 }),
loops.listComponents({ perPage: 20 }),
]);
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$themes = $loops->themes->list(per_page: 20);
$components = $loops->components->list(per_page: 20);
themes = LoopsSdk::Themes.list(perPage: 20)
components = LoopsSdk::Components.list(perPage: 20)
import requests
themes_response = requests.get(
"https://app.loops.so/api/v1/themes",
headers={
"Authorization": "Bearer <your-api-key>",
},
params={"perPage": 20},
)
components_response = requests.get(
"https://app.loops.so/api/v1/components",
headers={
"Authorization": "Bearer <your-api-key>",
},
params={"perPage": 20},
)
themes = themes_response.json()
components = components_response.json()
Update the draft email message with contentRevisionId
Use draftEmailMessageId from when you created the transactional as the path parameter, and pass draftEmailMessageContentRevisionId as expectedRevisionId.
Apply styles or a theme in <Style />, and build the email using LMX elements.
Themes and components you queried in the previous step can be referenced by their IDs.
Save the returned
contentRevisionId after each update. Pass it as
expectedRevisionId on the next update to avoid 409 Conflict errors caused
by stale revisions.CLI reference
loops email-messages update $draftEmailMessageId \
--expected-revision-id $draftEmailMessageContentRevisionId \
--subject "Reset your password" \
--preview-text "Your password reset link" \
--from-name "Loops" \
--from-email hello \
--reply-to support@example.com \
--lmx-file ./email.lmx
const lmxContent = `
<Style themeId="default" />
<Paragraph>
<Text>Click the link below to reset your password.</Text>
</Paragraph>
<Component componentId="logo" />
<Section>
...
</Section>`;
const response = await fetch(
`https://app.loops.so/api/v1/email-messages/${draftEmailMessageId}`,
{
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
expectedRevisionId: draftEmailMessageContentRevisionId,
subject: "Reset your password",
previewText: "Your password reset link",
fromName: "Loops",
fromEmail: "hello",
replyToEmail: "support@example.com",
lmx: lmxContent,
}),
},
);
const updated = await response.json();
const nextContentRevisionId = updated.contentRevisionId;
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const lmxContent = `
<Style themeId="default" />
<Paragraph>
<Text>Click the link below to reset your password.</Text>
</Paragraph>
<Component componentId="logo" />
<Section>
...
</Section>`;
const updated = await loops.updateEmailMessage(draftEmailMessageId, {
expectedRevisionId: draftEmailMessageContentRevisionId,
subject: "Reset your password",
previewText: "Your password reset link",
fromName: "Loops",
fromEmail: "hello",
replyToEmail: "support@example.com",
lmx: lmxContent,
});
const nextContentRevisionId = updated.contentRevisionId;
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$lmx_content = <<<LMX
<Style themeId="default" />
<Paragraph>
<Text>Click the link below to reset your password.</Text>
</Paragraph>
<Component componentId="logo" />
<Section>
...
</Section>
LMX;
$updated = $loops->emailMessages->update(
email_message_id: $draft_email_message_id,
expected_revision_id: $draft_email_message_content_revision_id,
subject: 'Reset your password',
preview_text: 'Your password reset link',
from_name: 'Loops',
from_email: 'hello',
reply_to_email: 'support@example.com',
lmx: $lmx_content,
);
$next_content_revision_id = $updated['contentRevisionId'];
lmx_content = <<~LMX
<Style themeId="default" />
<Paragraph>
<Text>Click the link below to reset your password.</Text>
</Paragraph>
<Component componentId="logo" />
<Section>
...
</Section>
LMX
updated = LoopsSdk::EmailMessages.update(
email_message_id: draft_email_message_id,
expected_revision_id: draft_email_message_content_revision_id,
subject: "Reset your password",
preview_text: "Your password reset link",
from_name: "Loops",
from_email: "hello",
reply_to_email: "support@example.com",
lmx: lmx_content,
)
next_content_revision_id = updated["contentRevisionId"]
import requests
response = requests.post(
f"https://app.loops.so/api/v1/email-messages/{draft_email_message_id}",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"expectedRevisionId": draft_email_message_content_revision_id,
"subject": "Reset your password",
"previewText": "Your password reset link",
"fromName": "Loops",
"fromEmail": "hello",
"replyToEmail": "support@example.com",
"lmx": "<Email><Style backgroundColor=\"#ffffff\" textBaseColor=\"#111111\" /><Section><Text>Click the link below to reset your password.</Text></Section></Email>",
},
)
updated = response.json()
next_content_revision_id = updated["contentRevisionId"]
Send a transactional preview
After updating the draft email message, send a test preview to one or more addresses before publishing. Transactional previews acceptdataVariables for
personalization.
Use draftEmailMessageId from when you created the transactional as the path
parameter.
There is no CLI command for previews yet — use the API below.
API reference
const previewResponse = await fetch(
`https://app.loops.so/api/v1/email-messages/${draftEmailMessageId}/preview`,
{
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
emails: ["you@example.com"],
dataVariables: {
loginUrl: "https://example.com/reset?token=abc123",
firstName: "Alex",
},
}),
},
);
const preview = await previewResponse.json();
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const preview = await loops.sendEmailMessagePreview(draftEmailMessageId, {
emails: ["you@example.com"],
dataVariables: {
loginUrl: "https://example.com/reset?token=abc123",
firstName: "Alex",
},
});
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$preview = $loops->emailMessages->preview(
email_message_id: $draft_email_message_id,
emails: ['you@example.com'],
data_variables: [
'loginUrl' => 'https://example.com/reset?token=abc123',
'firstName' => 'Alex',
],
);
preview = LoopsSdk::EmailMessages.preview(
email_message_id: draft_email_message_id,
emails: ["you@example.com"],
data_variables: {
loginUrl: "https://example.com/reset?token=abc123",
firstName: "Alex",
},
)
import requests
preview_response = requests.post(
f"https://app.loops.so/api/v1/email-messages/{draft_email_message_id}/preview",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"emails": ["you@example.com"],
"dataVariables": {
"loginUrl": "https://example.com/reset?token=abc123",
"firstName": "Alex",
},
},
)
preview = preview_response.json()
Upload an image asset
If your LMX includes<Image /> tags, upload image files with the Upload API
and use the returned finalUrl as the image src.
Create upload API referenceComplete upload API reference Upload CLI reference
loops uploads create ./header.png -o json
const createResponse = await fetch("https://app.loops.so/api/v1/uploads", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
contentType: "image/png",
contentLength: imageBuffer.byteLength,
}),
});
const { emailAssetId, presignedUrl } = await createResponse.json();
await fetch(presignedUrl, {
method: "PUT",
headers: {
"Content-Type": "image/png",
"Content-Length": String(imageBuffer.byteLength),
},
body: imageBuffer,
});
const completeResponse = await fetch(
`https://app.loops.so/api/v1/uploads/${emailAssetId}/complete`,
{
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
},
},
);
const { finalUrl } = await completeResponse.json();
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const { emailAssetId, presignedUrl } = await loops.createUpload({
contentType: "image/png",
contentLength: imageBuffer.byteLength,
});
await fetch(presignedUrl, {
method: "PUT",
headers: {
"Content-Type": "image/png",
"Content-Length": String(imageBuffer.byteLength),
},
body: imageBuffer,
});
const { finalUrl } = await loops.completeUpload(emailAssetId);
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$result = $loops->uploads->upload(path: './header.png');
$final_url = $result['finalUrl'];
response = LoopsSdk::Uploads.upload(path: "./header.png")
final_url = response["finalUrl"]
import requests
create_response = requests.post(
"https://app.loops.so/api/v1/uploads",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"contentType": "image/png",
"contentLength": len(image_bytes),
},
)
create_data = create_response.json()
email_asset_id = create_data["emailAssetId"]
presigned_url = create_data["presignedUrl"]
requests.put(
presigned_url,
headers={
"Content-Type": "image/png",
"Content-Length": str(len(image_bytes)),
},
data=image_bytes,
)
complete_response = requests.post(
f"https://app.loops.so/api/v1/uploads/{email_asset_id}/complete",
headers={
"Authorization": "Bearer <your-api-key>",
},
)
final_url = complete_response.json()["finalUrl"]
Publish the transactional email
Publish the draft email message so it can be sent with the transactional send endpoint. There is no CLI command for publishing transactional emails yet — use the API below. API referenceconst publishResponse = await fetch(
`https://app.loops.so/api/v1/transactional-emails/${id}/publish`,
{
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
},
},
);
const published = await publishResponse.json();
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const published = await loops.publishTransactionalEmail(transactionalId);
use Loops\LoopsClient;
$loops = new LoopsClient('<your-api-key>');
$published = $loops->transactional->publish(transactional_id: $transactional_id);
published = LoopsSdk::Transactional.publish(
transactional_id: transactional_id,
)
import requests
publish_response = requests.post(
f"https://app.loops.so/api/v1/transactional-emails/{id}/publish",
headers={
"Authorization": "Bearer <your-api-key>",
},
)
published = publish_response.json()
Send a transactional email
Use the transactionalid from create (or publish) as transactionalId in the send request.
API referenceCLI reference
loops transactional send <transactional-id> \
--email test@example.com \
--var loginUrl=https://example.com/login
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: id,
dataVariables: {
loginUrl: "https://example.com/login",
},
}),
});
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "<transactional-id>",
dataVariables: {
loginUrl: "https://example.com/login",
},
});
use Loops\LoopsClient;
$loops = new LoopsClient("<your-api-key>");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_id: '<transactional-id>',
data_variables: [
'loginUrl' => 'https://example.com/login',
],
);
response = LoopsSdk::Transactional.send(
email: "test@example.com",
transactional_id: "<transactional-id>",
data_variables: {
loginUrl: "https://example.com/login",
},
)
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": id,
"dataVariables": {
"loginUrl": "https://example.com/login",
},
},
)
Send a transactional email with an array data variable
Learn more about arrays. API referenceCLI reference
loops transactional send <id> \
--email test@example.com \
--json-vars ./vars.json
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
items: [
{ name: "Item 1", description: "Description of Item 1" },
{ name: "Item 2", description: "Description of Item 2" },
],
},
}),
});
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
items: [
{ name: "Item 1", description: "Description of Item 1" },
{ name: "Item 2", description: "Description of Item 2" },
],
},
});
use Loops\LoopsClient;
$loops = new LoopsClient("<your-api-key>");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_id: '<id>',
data_variables: [
'items' => [
[
'name' => 'Item 1',
'description' => 'Description of Item 1',
],
[
'name' => 'Item 2',
'description' => 'Description of Item 2',
],
],
],
);
response = LoopsSdk::Transactional.send(
email: "test@example.com",
transactional_id: "<id>",
data_variables: {
items: [
{ name: "Item 1", description: "Description of Item 1" },
{ name: "Item 2", description: "Description of Item 2" },
],
},
)
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": "<id>",
"dataVariables": {
"items": [
{ "name": "Item 1", "description": "Description of Item 1" },
{ "name": "Item 2", "description": "Description of Item 2" },
],
},
},
)
Send a transactional email with attachments
You must request attachments to be enabled in your account before you can send emails with them.
CLI reference
loops transactional send <id> \
--email test@example.com \
--var loginUrl=https://example.com/login \
--attachment ./example.pdf
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: "example.pdf",
contentType: "application/pdf",
data: "<base64-encoded-file-content>",
},
],
}),
});
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: "example.pdf",
contentType: "application/pdf",
data: "<base64-encoded-file-content>",
},
],
});
use Loops\LoopsClient;
$loops = new LoopsClient("<your-api-key>");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_id: '<id>',
data_variables: [
'loginUrl' => 'https://example.com/login',
],
attachments: [
[
'filename' => 'example.pdf',
'content_type' => 'application/pdf',
'data' => base64_encode(file_get_contents('path/to/example.pdf')),
],
],
);
response = LoopsSdk::Transactional.send(
email: "test@example.com",
transactional_id: "<id>",
data_variables: {
loginUrl: "https://example.com/login",
},
attachments: [
{
filename: 'example.pdf',
content_type: 'application/pdf',
data: '<base64-encoded-file-content>',
},
],
)
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
},
json={
"email": "test@example.com",
"transactionalId": "<id>",
"dataVariables": {
"loginUrl": "https://example.com/login",
},
"attachments": [
{
"filename": "example.pdf",
"contentType": "application/pdf",
"data": "<base64-encoded-file-content>",
},
],
},
)
Send a transactional email with an idempotency key
Add anIdempotency-Key header to the request to prevent duplicate requests.
API referenceCLI reference
loops transactional send <id> \
--email test@example.com \
--var loginUrl=https://example.com/login \
--idempotency-key 550e8400-e29b-41d4-a716-446655440000
await fetch("https://app.loops.so/api/v1/transactional", {
method: "POST",
headers: {
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
body: JSON.stringify({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
loginUrl: "https://example.com/login",
},
}),
});
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const response = await loops.sendTransactionalEmail({
email: "test@example.com",
transactionalId: "<id>",
dataVariables: {
loginUrl: "https://example.com/login",
},
headers: {
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
});
use Loops\LoopsClient;
$loops = new LoopsClient("<your-api-key>");
$result = $loops->transactional->send(
email: 'test@example.com',
transactional_id: '<id>',
data_variables: [
'loginUrl' => 'https://example.com/login',
],
headers: [
'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000',
],
);
response = LoopsSdk::Transactional.send(
email: "test@example.com",
transactional_id: "<id>",
data_variables: {
loginUrl: "https://example.com/login",
},
headers: {
'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000',
},
)
import requests
response = requests.post(
"https://app.loops.so/api/v1/transactional",
headers={
"Authorization": "Bearer <your-api-key>",
"Content-Type": "application/json",
"Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000",
},
json={
"email": "test@example.com",
"transactionalId": "<id>",
"dataVariables": {
"loginUrl": "https://example.com/login",
},
},
)
List transactional emails
API referenceCLI reference
loops transactional list -o json
await fetch("https://app.loops.so/api/v1/transactional-emails", {
method: "GET",
headers: {
"Authorization": "Bearer <your-api-key>",
},
});
import { LoopsClient } from "loops";
const loops = new LoopsClient("<your-api-key>");
const response = await loops.listTransactionalEmails();
use Loops\LoopsClient;
$loops = new LoopsClient("<your-api-key>");
$result = $loops->transactional->list();
response = LoopsSdk::Transactional.list
import requests
response = requests.get(
"https://app.loops.so/api/v1/transactional-emails",
headers={
"Authorization": "Bearer <your-api-key>",
},
)

