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

# Migrate From Loops to Brew

> Move contacts, workflows, and code-wired transactional sends from Loops into Brew, including the API swap and a cutover order that avoids double-sends.

Loops customers are usually more code-integrated than a typical marketing-tool switch. Transactional sends, password resets, magic links, and 2FA codes are often wired straight into application code through the API.

Campaigns and workflows get built separately in the dashboard. This guide covers both halves.

It assumes your Brew brand and sending domain are already set up. If not, [verify your domain](/get-started/verify-your-sending-domain) first. DNS can take up to 72 hours.

Rule for the whole migration: **only one platform sends live email to a given contact at a time.**

## Concepts

| Loops                    | Brew                                                                                      |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| Contact                  | Contact                                                                                   |
| Contact property         | Custom property                                                                           |
| Mailing list             | Custom property plus an Audience filter, see Step 4                                       |
| Filter / Segment         | [Audience](/audience/create-audiences) (dynamic filter)                                   |
| Campaign                 | [Email](/create-emails/emails)                                                            |
| Workflow                 | [Automation](/create-emails/automations)                                                  |
| Transactional email      | An automation fired by a trigger event                                                    |
| Event                    | Trigger event (custom HTTP trigger or integration event)                                  |
| `dataVariables`, `{tag}` | `{{ property \| fallback }}`                                                              |
| `transactionalId`        | `triggerEventId` for the fire call, `emailId` plus `emailVersionId` inside the automation |

## 1. Export Contacts From Loops

<Warning>
  Every marketing recipient must have opted in to hear from you. Importing or syncing contact data into Brew is not permission to email it. See [What You Can Send](/get-started/what-you-can-send).
</Warning>

Audience page → filter to a segment if you want a subset → **Export**. Loops writes one CSV of whatever's in view, with columns for default and custom properties only. There's no separate export per subscription status, so filter by `Subscribed` first if you only want active contacts.

Carries over cleanly: email, name, source, custom properties, and `subscribed` (true/false).

**Mailing list membership doesn't carry over automatically.** Loops' export includes contact properties, not list membership. A straight export and import won't preserve which lists someone belonged to.

If that matters, filter to each list and export it separately, then reconstruct it in Step 4.

Event history and workflow enrollment state don't carry over either. Those live in Loops with no Brew equivalent to import into.

One reassuring parallel: in Loops, unsubscribing blocks campaigns and workflows while transactional email still goes through. Brew works the same way, so that part of the behavior your users already expect doesn't change.

## 2. Map Your CSV

| Column                        | Required                                |
| ----------------------------- | --------------------------------------- |
| `Email`                       | Yes                                     |
| `First Name`, `Last Name`     | Recommended                             |
| `Subscribed` (`true`/`false`) | No, defaults `true`                     |
| Anything else                 | Becomes a custom property automatically |

Notes:

* `userGroup` and any custom properties become Brew custom properties. Bring `userId` too if anything downstream matches on it
* These names are reserved and can't be used for custom properties: `email`, `first_name`, `last_name`, `subscribed`, `created_at`, `updated_at`, `validation_status`, `last_validated_at`, `import_id`
* Mailing list membership needs its own export per list (see Step 1), then its own import into the matching property. It won't ride along with a general contact import

**Personalization.** Loops uses single curly braces. `{firstName}` works in campaigns and workflows, `{EVENT_PROPERTY:name}` in workflow emails triggered by events, and `{DATA_VARIABLE:name}` in transactional emails.

The first becomes Brew's `{{ firstName | there }}`. The property name is case-sensitive, and the fallback shows when the value is missing.

The other two become trigger payload references, written the same way: `{{ orderId }}`. See [merge tags](/create-emails/automations#personalization).

One difference worth knowing: in Loops, a missing property value with no fallback means the email doesn't send. In Brew the fallback lives inside the tag itself. There's no separate step where you could forget one and have a send fail silently.

## 3. Import Into Brew

Audience → Add contacts → CSV Upload. Map columns, tick [validation](/audience/validate-contacts) if the list hasn't been checked recently, review, then import. Full import behavior is in [Add Contacts](/audience/add-contacts).

## 4. Rebuild Segmentation

**Filters and Segments** map to Brew [Audiences](/audience/create-audiences), dynamic filters over contact properties, email engagement, and subscription status. Describe one in plain English or build the filters by hand.

**Mailing Lists don't have a direct equivalent.** Brew has one subscription state per contact, subscribed or unsubscribed. There are no opt-in categories and no subscriber-facing preference center. Marketing emails and automations respect that flag, and transactional sends bypass it.

Rebuild each list as a custom property plus an Audience filtered on it. You keep the targeting, but membership is yours to manage. Contacts can't pick lists for themselves, and unsubscribing opts them out of all marketing email rather than one list. If a Loops list carried a real consent distinction, decide how to honor it before your first send.

## 5. Rebuild Templates

| You have                           | Use in Brew                                                                 |
| ---------------------------------- | --------------------------------------------------------------------------- |
| A Loops campaign or workflow email | Screenshot it and use Upload image to recreate, or prompt Brew directly     |
| Custom MJML                        | No direct MJML import. Rebuild in Brew's editor or upload the rendered HTML |
| A Figma source                     | Connect with Figma                                                          |

Open **Emails** in the sidebar, then use the **Import** tools on the right side of the canvas. See [Importing existing designs](/create-emails/emails#importing-existing-designs).

Loops themes, the shared styling across transactional emails, don't carry over as a concept. Brew's [brand identity](/brand/brand-identity) replaces the need for a manually maintained theme. Set it once and every rebuilt template inherits your logo, colors, fonts, and voice.

## 6. Rebuild Workflows as Automations

| Loops trigger                                                                    | Brew equivalent                                                                                                                                                     |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contact added (fires on new contacts from an integration, form, or API, not CSV) | Integration event if the source is a native Brew integration such as Stripe, Clerk, or Shopify, otherwise a custom HTTP trigger fired from your own signup endpoint |
| Contact updated                                                                  | Custom HTTP trigger fired from your own update logic                                                                                                                |
| Contact added to list                                                            | Custom HTTP trigger fired when you set the matching property                                                                                                        |
| Event received                                                                   | Custom HTTP trigger, or an integration event if it's already a native Brew integration                                                                              |

Loops has a trick for re-running an existing contact through a workflow: download a CSV, then re-upload it with "Trigger workflows" toggled on. That maps to a [manual-audience run](/create-emails/automations#manual-audience) in Brew, the one-off, on-demand option, not a live trigger. Manual-audience automations are launched with `POST /v1/automations/{automationId}/run` rather than published.

Branches become Filter or Split nodes, and delay steps become Wait nodes. Test-fire before publishing, and leave every rebuilt automation unpublished until cutover. That's what prevents a double-send.

## 7. Swap Your Code Over to Triggers

Loops' transactional call is one request: a `transactionalId`, the recipient, and `dataVariables`. Brew has no single-call equivalent. Every per-recipient send, marketing or transactional, goes through the same trigger and automation system.

That matters even if you never sent transactional email from Loops. A cart-abandonment or trial-ending email is a marketing send fired per recipient by a custom event. It uses this exact mechanism, and transactional email is one case of it, not a separate system.

Set each one up once:

1. `POST /v1/automations/triggers` to create the trigger event, for example "Password reset requested". Capture the `triggerEventId`. You can also create it in-app from **Trigger events**.
2. `POST /v1/emails` to generate the email design. Capture both the `emailId` and the `emailVersionId`.
3. `GET /v1/domains` for a verified `domainId`.
4. `POST /v1/automations` with a Send Email node wired to that trigger. Each sendEmail node carries `emailId`, `emailVersionId`, `domainId`, `subject`, and `previewText`.
5. `PATCH /v1/automations/{automationId}` with `{ "published": true }`.
6. Replace the Loops call in your backend with a fire request.

Before, with Loops:

```js theme={null}
await loops.sendTransactionalEmail({
  transactionalId: process.env.LOOPS_RESET_TEMPLATE_ID,
  email: user.email,
  dataVariables: { resetUrl },
});
```

After, with Brew:

```js theme={null}
await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_RESET_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `reset-${resetToken.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, resetUrl },
    }),
  },
);
```

Set an `Idempotency-Key` on every retried call, the same discipline you used in Loops. Derive it from something stable about the action, like the reset token id above.

A key built from a timestamp changes on every retry, which defeats the point and lets a retry send twice. [Idempotency](/api-reference/api/idempotency) covers the replay window and conflict behavior.

Full request shapes are in the [API reference](/api-reference/api/api-introduction). This is the shape of the migration, not a copy-paste replacement for it.

## 8. Cut Over

Loops mixes transactional and marketing on one audience, so a single flip-the-switch day is riskier than it needs to be. Move one email type at a time.

1. Warm up your Brew sending domain before any real volume, most engaged contacts first, ramped over several days. [Gradual send](/create-emails/send-options#gradual-send) does the ramping for you.
2. Import contacts and rebuild templates and automations while Loops keeps sending everything as normal.
3. Cut over your lowest-risk marketing email first, a newsletter rather than a password reset, to confirm delivery and rendering.
4. Move transactional email types one at a time, reset link first, then receipts, then whatever's left. Watch each one in [analytics](/analytics/reading-analytics) before moving the next.
5. Move workflows last, once their triggers are confirmed firing correctly.
6. Point signup forms and inbound integrations at Brew once everything upstream is verified.
7. Keep Loops active but unused for a couple of weeks as a fallback, then downgrade or cancel.

Never leave a transactional email type live in both places at once. That's how a user gets two password reset emails with two different links.

## Quick Reference

| Task                   | Where in Brew                                      |
| ---------------------- | -------------------------------------------------- |
| Import contacts        | Audience → Add contacts → CSV Upload               |
| Build a segment        | Audience → Create Audience                         |
| Import a template      | Emails → canvas → Import                           |
| Build a flow           | Emails → Automations tab                           |
| Create a trigger event | Trigger events, or `POST /v1/automations/triggers` |
| API keys               | Settings → API                                     |

## Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to explore it further with ChatGPT or Claude.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
