Skip to main content

Create a transactional email

This creates a transactional email and a related draft email message in one request. Only a name 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.
API reference
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;

Query themes and components for your LMX

You can fetch your available themes and reusable components before building the lmx payload. List themes API reference
List components API reference
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();

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.
API reference
Get theme API reference
Get component API reference
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;

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 reference
Complete upload API reference
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();

Publish the transactional email

Publish the draft email message so it can be sent with the transactional send endpoint. API reference
const publishResponse = await fetch(
  `https://app.loops.so/api/v1/transactional-emails/${transactionalId}/publish`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
);

const published = await publishResponse.json();

Send a transactional email

Use the transactional id from create (or publish) as transactionalId in the send request. API reference
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: transactionalId,
    dataVariables: {
      loginUrl: "https://example.com/login",
    },
  }),
});

Send a transactional email with an array data variable

Learn more about arrays. API reference
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: "<transactional-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.
API reference
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: "<transactional-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 an Idempotency-Key header to the request to prevent duplicate requests. API reference
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: "<transactional-id>",
    dataVariables: {
      loginUrl: "https://example.com/login",
    },
  }),
});

List transactional emails

API reference
await fetch("https://app.loops.so/api/v1/transactional-emails", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <your-api-key>",
  },
});
Last modified on June 8, 2026