> ## Documentation Index
> Fetch the complete documentation index at: https://loops.so/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Create and send your first campaign with the API or an agent

> Create a draft campaign, write LMX content, and preview it in Loops using the Loops CLI, API, or an AI agent.

This guide walks through the fastest path to your first campaign via API:

`API key → create campaign → write LMX → preview → send`

By the end, you'll have a draft campaign with real email content that you can open in Loops, preview, choose an audience for, and send.

Each step includes examples for the [Loops CLI](/docs/cli), curl, and the [JavaScript SDK](/docs/sdks/javascript).

The CLI is especially useful for creating emails inside coding agents. Check out Loops' [Agent skills](/docs/skills) for more information.

## Prerequisites

Before you start, make sure you have:

* A Loops account with a [verified sending domain](/docs/sending-domain)
* At least one contact in your audience (or a [mailing list](/docs/contacts/mailing-lists) to send to later)
* An API key from [Settings → API](https://app.loops.so/settings?page=api)
* For SDK examples: `npm install loops` ([JavaScript SDK](/docs/sdks/javascript))

Authenticate and verify your key:

<CodeGroup>
  ```bash CLI theme={"dark"}
  curl -fsSL https://install.loops.so/cli | sh
  loops auth login my-team
  loops api-key
  ```

  ```bash curl theme={"dark"}
  export LOOPS_API_KEY="your-api-key-here"

  curl -s https://app.loops.so/api/v1/api-key \
    -H "Authorization: Bearer $LOOPS_API_KEY"
  ```

  ```js JavaScript SDK theme={"dark"}
  import { LoopsClient } from "loops";

  const loops = new LoopsClient(process.env.LOOPS_API_KEY);

  const data = await loops.testApiKey();
  // { success: true, teamName: "..." }
  ```
</CodeGroup>

Never commit your API key to source control. The CLI stores keys locally via `loops auth login`; curl and SDK examples use the `LOOPS_API_KEY` environment variable.

## What you'll build

You'll create a product update campaign named **March product update** with a short LMX body, then open it in the Loops dashboard to preview and send.

## Step 1: Create a draft campaign

Creating a campaign also creates an empty email message. Save the IDs from the response. You'll need them in the next steps.

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops campaigns create --name "March product update" -o json
  ```

  ```bash curl theme={"dark"}
  curl -s -X POST https://app.loops.so/api/v1/campaigns \
    -H "Authorization: Bearer $LOOPS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name": "March product update"}'
  ```

  ```js JavaScript SDK theme={"dark"}
  import { LoopsClient } from "loops";

  const loops = new LoopsClient(process.env.LOOPS_API_KEY);

  const campaign = await loops.createCampaign({
    name: "March product update",
  });
  ```
</CodeGroup>

The response includes:

| Field                           | Use it for                                        |
| ------------------------------- | ------------------------------------------------- |
| `id`                            | Campaign ID — open this in the dashboard          |
| `emailMessageId`                | Updating the email content                        |
| `emailMessageContentRevisionId` | Pass as `expectedRevisionId` on your first update |

```bash theme={"dark"}
export CAMPAIGN_ID="cln0y4p6r003yl70i1j2k3l4m"
export EMAIL_MESSAGE_ID="cmn5zia4i0017tzli8ric8giv"
export REVISION_ID="rev_01h..."
```

<Tip>
  Save `emailMessageContentRevisionId` from the create response. Pass it as
  `expectedRevisionId` (or `--expected-revision-id` in the CLI) when updating
  the email message.
</Tip>

## Step 2: Write LMX content

[LMX](/docs/creating-emails/lmx) (Loops Markup Language) is how you define email body content for the API. Create a file called `email.lmx`:

```xml theme={"dark"}
<Style backgroundColor="#ffffff" textBaseColor="#111111" />
<H1>March product update</H1>
<Paragraph>
  Hi {contact.firstName}, here is what we shipped this month.
</Paragraph>
<Section>
  <Paragraph>
    <Text fontWeight="bold">New:</Text> Campaign creation via API
  </Paragraph>
  <Paragraph>
    <Text fontWeight="bold">Improved:</Text> Faster preview rendering
  </Paragraph>
</Section>
<Button href="https://loops.so/changelog">Read the changelog</Button>
```

You can also reference [themes](/docs/api-reference/list-themes) and [components](/docs/api-reference/list-components) by ID instead of inline styles. See the [LMX reference](/docs/creating-emails/lmx) for all available tags.

## Step 3: Update the email message

Push your LMX content and sending settings to the email message created in step 1.

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops email-messages update $EMAIL_MESSAGE_ID \
    --expected-revision-id $REVISION_ID \
    --subject "What we shipped in March" \
    --preview-text "New features and improvements" \
    --from-name "Your Company" \
    --from-email hello \
    --lmx-file ./email.lmx
  ```

  ```bash curl theme={"dark"}
  curl -s -X POST "https://app.loops.so/api/v1/email-messages/$EMAIL_MESSAGE_ID" \
    -H "Authorization: Bearer $LOOPS_API_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n \
      --arg rev "$REVISION_ID" \
      --arg lmx "$(cat email.lmx)" \
      '{
        expectedRevisionId: $rev,
        subject: "What we shipped in March",
        previewText: "New features and improvements",
        fromName: "Your Company",
        fromEmail: "hello",
        lmx: $lmx
      }')"
  ```

  ```js JavaScript SDK theme={"dark"}
  import { readFileSync } from "node:fs";
  import { LoopsClient } from "loops";

  const loops = new LoopsClient(process.env.LOOPS_API_KEY);
  const lmx = readFileSync("email.lmx", "utf8");

  const updated = await loops.updateEmailMessage(emailMessageId, {
    expectedRevisionId: revisionId,
    subject: "What we shipped in March",
    previewText: "New features and improvements",
    fromName: "Your Company",
    fromEmail: "hello",
    lmx,
  });
  ```
</CodeGroup>

`fromEmail` is the username only (the part before `@`). Loops appends your team's sending domain automatically.

<Tip>
  For agent scripts where you don't have a saved revision ID, use
  `--force` instead of `--expected-revision-id`. This fetches the current
  revision automatically but overwrites any concurrent edits.
</Tip>

## Step 4: Preview in the dashboard

Open your campaign in Loops:

```
https://app.loops.so/campaigns/$CAMPAIGN_ID
```

Click into the email editor. Your LMX content renders in the editor. You can tweak copy, adjust styles, or add images from here.

You can also make edits via the API, CLI, or an AI tool, and see the changes happen in real-time in the editor.

To send a test to your inbox, click the **Send a preview** icon in the top right of the editor. Pick a contact (or enter test data) and send.

You can also send a preview via the CLI, API, or JavaScript SDK:

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops email-messages preview $EMAIL_MESSAGE_ID \
    --email you@example.com \
    --contact-prop firstName=Alex
  ```

  ```bash curl theme={"dark"}
  curl -s -X POST \
    "https://app.loops.so/api/v1/email-messages/$EMAIL_MESSAGE_ID/preview" \
    -H "Authorization: Bearer $LOOPS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "emails": ["you@example.com"],
      "contactProperties": { "firstName": "Alex" }
    }'
  ```

  ```js JavaScript SDK theme={"dark"}
  import { LoopsClient } from "loops";

  const loops = new LoopsClient(process.env.LOOPS_API_KEY);

  const preview = await loops.sendEmailMessagePreview(emailMessageId, {
    emails: ["you@example.com"],
    contactProperties: { firstName: "Alex" },
  });
  ```
</CodeGroup>

## Step 5: Choose an audience and send

Back in the dashboard, click **Next** to choose who receives the campaign. Pick a [mailing list](/docs/contacts/mailing-lists), [segment](/docs/contacts/filters-segments), or your full audience.

On the final step, send now or schedule for later.

You can also set the audience and scheduling preference on the draft via the API or CLI with [Update a campaign](/docs/api-reference/update-campaign). Open the campaign in the dashboard to publish and send. Use `loops lists list -o json` to find a mailing list ID:

<CodeGroup>
  ```bash CLI theme={"dark"}
  loops lists list -o json

  loops campaigns update $CAMPAIGN_ID \
    --mailing-list-id your-mailing-list-id \
    --schedule-now
  ```

  ```bash curl theme={"dark"}
  curl -s -X POST "https://app.loops.so/api/v1/campaigns/$CAMPAIGN_ID" \
    -H "Authorization: Bearer $LOOPS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "mailingListId": "your-mailing-list-id",
      "scheduling": { "method": "now" }
    }'
  ```

  ```js JavaScript SDK theme={"dark"}
  import { LoopsClient } from "loops";

  const loops = new LoopsClient(process.env.LOOPS_API_KEY);

  const updated = await loops.updateCampaign(campaignId, {
    mailingListId: "your-mailing-list-id",
    scheduling: { method: "now" },
  });
  ```
</CodeGroup>

<Warning>
  Sending requires a verified sending domain and a valid audience. Test sending to `@example.com` addresses first if you want to validate the flow without
  delivering real email.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="LMX reference" icon="square-code" href="/docs/creating-emails/lmx" />

  <Card title="Campaign API examples" icon="code" href="/docs/api-reference/examples/campaigns" />

  <Card title="Create campaign API" icon="send" href="/docs/api-reference/create-campaign" />

  <Card title="JavaScript SDK" icon="js" href="/docs/sdks/javascript" />

  <Card title="Loops CLI" icon="terminal" href="/docs/cli" />
</CardGroup>


## Related topics

- [Your first onboarding emails](/docs/guides/onboarding-emails.md)
- [LMX](/docs/creating-emails/lmx.md)
- [Agent skills for Loops](/docs/skills.md)
- [Filters and Segments](/docs/contacts/filters-segments.md)
- [Campaigns](/docs/cli/campaigns.md)
