Skip to main content

Create a campaign

This creates a draft campaign and a related email message in one request. Only a name value is required.
Save the returned emailMessageContentRevisionId. Pass it as expectedRevisionId when updating an email message to avoid 409 Conflict errors caused by stale revisions.
API reference
const response = await fetch("https://app.loops.so/api/v1/campaigns", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <your-api-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Spring product announcement",
  }),
});

const data = await response.json();
const campaignId = data.id;
const emailMessageId = data.emailMessageId;
const emailMessageContentRevisionId = data.emailMessageContentRevisionId;

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();
Use emailMessageId from when you created the campaign as the path parameter, and pass the emailMessageContentRevisionId as expectedRevisionId. Apply styles or a theme in <Style />, and create an email using LMX elements. Themes and components you queried in step 2 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>Hey there, here is what's new.</Text>
</Paragraph>
<Component componentId="logo" />
<Section>
  <Paragraph>Read the full changelog in your dashboard.</Paragraph>
</Section>`;

const response = await fetch(
  `https://app.loops.so/api/v1/email-messages/${emailMessageId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      expectedRevisionId: emailMessageContentRevisionId,
      subject: "Big spring updates",
      previewText: "A quick look at what's new",
      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();

Target an audience segment

List your audience segments to find a segment ID, then apply it to a draft campaign with Update a campaign. Setting audienceSegmentId clears any existing audienceFilter on the campaign. API reference
const segmentsResponse = await fetch(
  "https://app.loops.so/api/v1/audience-segments?perPage=20",
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer <your-api-key>",
    },
  },
);

const { data: segments } = await segmentsResponse.json();
const audienceSegmentId = segments[0].id;

const updateResponse = await fetch(
  `https://app.loops.so/api/v1/campaigns/${campaignId}`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      audienceSegmentId,
    }),
  },
);

const updatedCampaign = await updateResponse.json();
You can also set the segment when creating the campaign:
const response = await fetch("https://app.loops.so/api/v1/campaigns", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <your-api-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Spring product announcement",
    audienceSegmentId: "seg_01hxyz",
  }),
});

Send a campaign preview

After updating the campaign’s email message, send a test preview to one or more addresses. Campaign previews accept contactProperties for personalization. Use emailMessageId from when you created the campaign as the path parameter. API reference
const previewResponse = await fetch(
  `https://app.loops.so/api/v1/email-messages/${emailMessageId}/preview`,
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer <your-api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      emails: ["you@example.com"],
      contactProperties: {
        firstName: "Alex",
        plan: "Pro",
      },
    }),
  },
);

const preview = await previewResponse.json();
Last modified on June 22, 2026